mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-28 20:56:11 +01:00
Compare commits
59 commits
fd8748dadc
...
35c1df0744
Author | SHA1 | Date | |
---|---|---|---|
35c1df0744 | |||
569a67327c | |||
aeffa031c0 | |||
146824badc | |||
eaa66f85f6 | |||
969a6ab24a | |||
7d59060dc6 | |||
308812a82e | |||
fc26becba4 | |||
013cc1dee4 | |||
6d0f2c1b82 | |||
2cccc02e76 | |||
356aa6521b | |||
cf323a3d55 | |||
6bab3c374c | |||
570e8cec9e | |||
bf810fa8d3 | |||
66dfb2813c | |||
95a8987844 | |||
9fd2df6e30 | |||
5406310f3e | |||
b21cc70dd7 | |||
4a5d9d4b78 | |||
1e1b162cbe | |||
b1bc294955 | |||
01ab0583f5 | |||
786dfc7fb8 | |||
061abe6004 | |||
3e3ef76808 | |||
7067cc7da4 | |||
e6bbecb02d | |||
b70196653f | |||
9508aa7713 | |||
1ce33aa38d | |||
0fa436c373 | |||
296935b0d7 | |||
1c25bbe773 | |||
c8d97e5594 | |||
e426a52a87 | |||
faa796feb9 | |||
cdc38ace39 | |||
4043377377 | |||
19c9e0a0c2 | |||
ef9a0c8d3d | |||
d1ad4dd561 | |||
b92863b024 | |||
91fda7ee81 | |||
8a4407ef72 | |||
a8beeff422 | |||
eda83cc7ed | |||
aea3c7d6e8 | |||
24028747d3 | |||
019e38a746 | |||
1f7a648057 | |||
c17b4bdaeb | |||
634519e891 | |||
d1520cf08d | |||
8206d509fc | |||
a5ba7cadf7 |
|
@ -56,14 +56,15 @@ jobs:
|
|||
image: 'code.forgejo.org/oci/node:20-bookworm'
|
||||
services:
|
||||
elasticsearch:
|
||||
image: docker.io/bitnami/elasticsearch:7
|
||||
image: code.forgejo.org/oci/bitnami/elasticsearch:7
|
||||
options: --tmpfs /bitnami/elasticsearch/data
|
||||
env:
|
||||
discovery.type: single-node
|
||||
ES_JAVA_OPTS: "-Xms512m -Xmx512m"
|
||||
minio:
|
||||
image: docker.io/bitnami/minio:2024.8.17
|
||||
image: code.forgejo.org/oci/bitnami/minio:2024.8.17
|
||||
options: >-
|
||||
--hostname gitea.minio
|
||||
--hostname gitea.minio --tmpfs /bitnami/minio/data
|
||||
env:
|
||||
MINIO_DOMAIN: minio
|
||||
MINIO_ROOT_USER: 123456
|
||||
|
@ -121,27 +122,35 @@ jobs:
|
|||
USE_REPO_TEST_DIR: 1
|
||||
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1
|
||||
CHANGED_FILES: ${{steps.changed-files.outputs.all_changed_files}}
|
||||
- name: Upload test artifacts on failure
|
||||
if: failure()
|
||||
uses: https://code.forgejo.org/forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: test-artifacts.zip
|
||||
path: tests/e2e/test-artifacts/
|
||||
retention-days: 3
|
||||
test-remote-cacher:
|
||||
if: vars.ROLE == 'forgejo-coding' || vars.ROLE == 'forgejo-testing'
|
||||
runs-on: docker
|
||||
needs: [backend-checks, frontend-checks, test-unit]
|
||||
container:
|
||||
image: 'code.forgejo.org/oci/node:20-bookworm'
|
||||
name: ${{ format('test-remote-cacher ({0})', matrix.cacher.name) }}
|
||||
strategy:
|
||||
matrix:
|
||||
cacher:
|
||||
# redis
|
||||
- image: docker.io/bitnami/redis:7.2
|
||||
port: 6379
|
||||
# redict
|
||||
- image: registry.redict.io/redict:7.3.0-scratch
|
||||
port: 6379
|
||||
# valkey
|
||||
- image: docker.io/bitnami/valkey:7.2
|
||||
port: 6379
|
||||
# garnet
|
||||
- image: ghcr.io/microsoft/garnet-alpine:1.0.14
|
||||
port: 6379
|
||||
- name: redis
|
||||
image: code.forgejo.org/oci/bitnami/redis:7.2
|
||||
options: --tmpfs /bitnami/redis/data
|
||||
- name: redict
|
||||
image: registry.redict.io/redict:7.3.0-scratch
|
||||
options: --tmpfs /data
|
||||
- name: valkey
|
||||
image: code.forgejo.org/oci/bitnami/valkey:7.2
|
||||
options: --tmpfs /bitnami/redis/data
|
||||
- name: garnet
|
||||
image: ghcr.io/microsoft/garnet-alpine:1.0.14
|
||||
options: --tmpfs /data
|
||||
services:
|
||||
cacher:
|
||||
image: ${{ matrix.cacher.image }}
|
||||
|
@ -169,14 +178,15 @@ jobs:
|
|||
image: 'code.forgejo.org/oci/node:20-bookworm'
|
||||
services:
|
||||
mysql:
|
||||
image: 'docker.io/bitnami/mysql:8.4'
|
||||
image: 'code.forgejo.org/oci/bitnami/mysql:8.4'
|
||||
env:
|
||||
ALLOW_EMPTY_PASSWORD: yes
|
||||
MYSQL_DATABASE: testgitea
|
||||
#
|
||||
# See also https://codeberg.org/forgejo/forgejo/issues/976
|
||||
#
|
||||
MYSQL_EXTRA_FLAGS: --innodb-adaptive-flushing=OFF --innodb-buffer-pool-size=4G --innodb-log-buffer-size=128M --innodb-flush-log-at-trx-commit=0 --innodb-flush-log-at-timeout=30 --innodb-flush-method=nosync --innodb-fsync-threshold=1000000000
|
||||
MYSQL_EXTRA_FLAGS: --innodb-adaptive-flushing=OFF --innodb-buffer-pool-size=4G --innodb-log-buffer-size=128M --innodb-flush-log-at-trx-commit=0 --innodb-flush-log-at-timeout=30 --innodb-flush-method=nosync --innodb-fsync-threshold=1000000000 --disable-log-bin
|
||||
options: --tmpfs /bitnami/mysql/data
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
|
@ -198,17 +208,19 @@ jobs:
|
|||
image: 'code.forgejo.org/oci/node:20-bookworm'
|
||||
services:
|
||||
minio:
|
||||
image: docker.io/bitnami/minio:2024.8.17
|
||||
image: code.forgejo.org/oci/bitnami/minio:2024.8.17
|
||||
env:
|
||||
MINIO_ROOT_USER: 123456
|
||||
MINIO_ROOT_PASSWORD: 12345678
|
||||
options: --tmpfs /bitnami/minio/data
|
||||
ldap:
|
||||
image: docker.io/gitea/test-openldap:latest
|
||||
image: code.forgejo.org/oci/test-openldap:latest
|
||||
pgsql:
|
||||
image: 'code.forgejo.org/oci/postgres:15'
|
||||
env:
|
||||
POSTGRES_DB: test
|
||||
POSTGRES_PASSWORD: postgres
|
||||
options: --tmpfs /var/lib/postgresql/data
|
||||
steps:
|
||||
- uses: https://code.forgejo.org/actions/checkout@v4
|
||||
- uses: ./.forgejo/workflows-composite/setup-env
|
||||
|
|
|
@ -6,6 +6,10 @@ A [patch or minor release](https://semver.org/spec/v2.0.0.html) (e.g. upgrading
|
|||
|
||||
The release notes of each release [are available in the corresponding milestone](https://codeberg.org/forgejo/forgejo/milestones), starting with [Forgejo 7.0.7](https://codeberg.org/forgejo/forgejo/milestone/7683) and [Forgejo 8.0.1](https://codeberg.org/forgejo/forgejo/milestone/7682).
|
||||
|
||||
## 9.0.2
|
||||
|
||||
The Forgejo v9.0.2 release notes are [available in the v9.0.2 milestone](https://codeberg.org/forgejo/forgejo/milestone/8610).
|
||||
|
||||
## 9.0.1
|
||||
|
||||
The Forgejo v9.0.1 release notes are [available in the v9.0.1 milestone](https://codeberg.org/forgejo/forgejo/milestone/8544).
|
||||
|
@ -163,6 +167,10 @@ A [companion blog post](https://forgejo.org/2024-07-release-v8-0/) provides addi
|
|||
- [PR](https://codeberg.org/forgejo/forgejo/pulls/2937): <!--number 2937 --><!--number--><!--description -->31 March updates<!--description-->
|
||||
<!--end release-notes-assistant-->
|
||||
|
||||
## 7.0.11
|
||||
|
||||
The Forgejo v7.0.11 release notes are [available in the v7.0.11 milestone](https://codeberg.org/forgejo/forgejo/milestone/8609).
|
||||
|
||||
## 7.0.10
|
||||
|
||||
The Forgejo v7.0.10 release notes are [available in the v7.0.10 milestone](https://codeberg.org/forgejo/forgejo/milestone/8286).
|
||||
|
|
8
go.mod
8
go.mod
|
@ -107,7 +107,7 @@ require (
|
|||
golang.org/x/sys v0.27.0
|
||||
golang.org/x/text v0.20.0
|
||||
golang.org/x/tools v0.26.0
|
||||
google.golang.org/grpc v1.67.1
|
||||
google.golang.org/grpc v1.68.0
|
||||
google.golang.org/protobuf v1.35.1
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
|
@ -283,7 +283,7 @@ require (
|
|||
golang.org/x/mod v0.21.0 // indirect
|
||||
golang.org/x/sync v0.9.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
|
@ -293,8 +293,8 @@ replace github.com/hashicorp/go-version => github.com/6543/go-version v1.3.1
|
|||
|
||||
replace github.com/shurcooL/vfsgen => github.com/lunny/vfsgen v0.0.0-20220105142115-2c99e1ffdfa0
|
||||
|
||||
replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.21.5
|
||||
replace github.com/nektos/act => code.forgejo.org/forgejo/act v1.22.0
|
||||
|
||||
replace github.com/mholt/archiver/v3 => code.forgejo.org/forgejo/archiver/v3 v3.5.1
|
||||
|
||||
replace github.com/goccy/go-json => github.com/grafana/go-json v0.0.0-20241106155216-71a03f133f5c
|
||||
replace github.com/goccy/go-json => github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2
|
||||
|
|
16
go.sum
16
go.sum
|
@ -4,8 +4,8 @@ code.forgejo.org/f3/gof3/v3 v3.7.0 h1:ZfuCP8CGm8ZJbWmL+V0pUu3E0X4FCAA7GfRDy/y5/K
|
|||
code.forgejo.org/f3/gof3/v3 v3.7.0/go.mod h1:oNhOeqD4DZYjVcNjQXIOdDX9b/1tqxi9ITLS8H9/Csw=
|
||||
code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251 h1:HTZl3CBk3ABNYtFI6TPLvJgGKFIhKT5CBk0sbOtkDKU=
|
||||
code.forgejo.org/forgejo-contrib/go-libravatar v0.0.0-20191008002943-06d1c002b251/go.mod h1:PphB88CPbx601QrWPMZATeorACeVmQlyv3u+uUMbSaM=
|
||||
code.forgejo.org/forgejo/act v1.21.5 h1:rWI+bhClocogdNwjRrM836rZYY7JBcHY3VUAwkYqEtw=
|
||||
code.forgejo.org/forgejo/act v1.21.5/go.mod h1:+PcvJ9iv+NTFeJSh79ra9Jbk9l0vvyA9D9me5/dbxYM=
|
||||
code.forgejo.org/forgejo/act v1.22.0 h1:NbUf0+vQ48+ddwe4zVkINqnxKYl/to+NUvW7iisPA60=
|
||||
code.forgejo.org/forgejo/act v1.22.0/go.mod h1:+PcvJ9iv+NTFeJSh79ra9Jbk9l0vvyA9D9me5/dbxYM=
|
||||
code.forgejo.org/forgejo/archiver/v3 v3.5.1 h1:UmmbA7D5550uf71SQjarmrn6yKwOGxtEjb3jaYYtmSE=
|
||||
code.forgejo.org/forgejo/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4=
|
||||
code.forgejo.org/forgejo/reply v1.0.2 h1:dMhQCHV6/O3L5CLWNTol+dNzDAuyCK88z4J/lCdgFuQ=
|
||||
|
@ -381,8 +381,8 @@ github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kX
|
|||
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||
github.com/grafana/go-json v0.0.0-20241106155216-71a03f133f5c h1:yKBKEC347YZpgii1KazRCfxHsTaxMqWZzoivM1OTT50=
|
||||
github.com/grafana/go-json v0.0.0-20241106155216-71a03f133f5c/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2 h1:8xGrYqQ1GM4aaMk7pNDfecBdL/VGhEbpvvGBoqO6BIY=
|
||||
github.com/grafana/go-json v0.0.0-20241115232854-f14426c40ff2/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/h2non/gock v1.2.0 h1:K6ol8rfrRkUOefooBC8elXoaNGYkpp7y2qcxGG6BzUE=
|
||||
github.com/h2non/gock v1.2.0/go.mod h1:tNhoxHYW2W42cYkYb1WqzdbYIieALC99kpYr7rH/BQk=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
|
@ -843,10 +843,10 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
|
|||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
|
||||
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
|
|
|
@ -15,12 +15,31 @@ import (
|
|||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type AuthorizationPurpose string
|
||||
|
||||
var (
|
||||
// Used to store long term authorization tokens.
|
||||
LongTermAuthorization AuthorizationPurpose = "long_term_authorization"
|
||||
|
||||
// Used to activate a user account.
|
||||
UserActivation AuthorizationPurpose = "user_activation"
|
||||
|
||||
// Used to reset the password.
|
||||
PasswordReset AuthorizationPurpose = "password_reset"
|
||||
)
|
||||
|
||||
// Used to activate the specified email address for a user.
|
||||
func EmailActivation(email string) AuthorizationPurpose {
|
||||
return AuthorizationPurpose("email_activation:" + email)
|
||||
}
|
||||
|
||||
// AuthorizationToken represents a authorization token to a user.
|
||||
type AuthorizationToken struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
UID int64 `xorm:"INDEX"`
|
||||
LookupKey string `xorm:"INDEX UNIQUE"`
|
||||
HashedValidator string
|
||||
Purpose AuthorizationPurpose `xorm:"NOT NULL DEFAULT 'long_term_authorization'"`
|
||||
Expiry timeutil.TimeStamp
|
||||
}
|
||||
|
||||
|
@ -41,7 +60,7 @@ func (authToken *AuthorizationToken) IsExpired() bool {
|
|||
// GenerateAuthToken generates a new authentication token for the given user.
|
||||
// It returns the lookup key and validator values that should be passed to the
|
||||
// user via a long-term cookie.
|
||||
func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp) (lookupKey, validator string, err error) {
|
||||
func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeStamp, purpose AuthorizationPurpose) (lookupKey, validator string, err error) {
|
||||
// Request 64 random bytes. The first 32 bytes will be used for the lookupKey
|
||||
// and the other 32 bytes will be used for the validator.
|
||||
rBytes, err := util.CryptoRandomBytes(64)
|
||||
|
@ -56,14 +75,15 @@ func GenerateAuthToken(ctx context.Context, userID int64, expiry timeutil.TimeSt
|
|||
Expiry: expiry,
|
||||
LookupKey: lookupKey,
|
||||
HashedValidator: HashValidator(rBytes[32:]),
|
||||
Purpose: purpose,
|
||||
})
|
||||
return lookupKey, validator, err
|
||||
}
|
||||
|
||||
// FindAuthToken will find a authorization token via the lookup key.
|
||||
func FindAuthToken(ctx context.Context, lookupKey string) (*AuthorizationToken, error) {
|
||||
func FindAuthToken(ctx context.Context, lookupKey string, purpose AuthorizationPurpose) (*AuthorizationToken, error) {
|
||||
var authToken AuthorizationToken
|
||||
has, err := db.GetEngine(ctx).Where("lookup_key = ?", lookupKey).Get(&authToken)
|
||||
has, err := db.GetEngine(ctx).Where("lookup_key = ? AND purpose = ?", lookupKey, purpose).Get(&authToken)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if !has {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
target_url: https://example.com/builds/
|
||||
description: My awesome CI-service
|
||||
context: ci/awesomeness
|
||||
context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7
|
||||
creator_id: 2
|
||||
|
||||
-
|
||||
|
@ -18,6 +19,7 @@
|
|||
target_url: https://example.com/coverage/
|
||||
description: My awesome Coverage service
|
||||
context: cov/awesomeness
|
||||
context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe
|
||||
creator_id: 2
|
||||
|
||||
-
|
||||
|
@ -29,6 +31,7 @@
|
|||
target_url: https://example.com/coverage/
|
||||
description: My awesome Coverage service
|
||||
context: cov/awesomeness
|
||||
context_hash: 3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe
|
||||
creator_id: 2
|
||||
|
||||
-
|
||||
|
@ -40,6 +43,7 @@
|
|||
target_url: https://example.com/builds/
|
||||
description: My awesome CI-service
|
||||
context: ci/awesomeness
|
||||
context_hash: c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7
|
||||
creator_id: 2
|
||||
|
||||
-
|
||||
|
@ -51,15 +55,41 @@
|
|||
target_url: https://example.com/builds/
|
||||
description: My awesome deploy service
|
||||
context: deploy/awesomeness
|
||||
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
|
||||
creator_id: 2
|
||||
|
||||
-
|
||||
id: 6
|
||||
index: 6
|
||||
index: 1
|
||||
repo_id: 62
|
||||
state: "failure"
|
||||
sha: "774f93df12d14931ea93259ae93418da4482fcc1"
|
||||
target_url: "/user2/test_workflows/actions"
|
||||
description: My awesome deploy service
|
||||
context: deploy/awesomeness
|
||||
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
|
||||
creator_id: 2
|
||||
|
||||
-
|
||||
id: 7
|
||||
index: 6
|
||||
repo_id: 1
|
||||
state: "pending"
|
||||
sha: "1234123412341234123412341234123412341234"
|
||||
target_url: https://example.com/builds/
|
||||
description: My awesome deploy service
|
||||
context: deploy/awesomeness
|
||||
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
|
||||
creator_id: 2
|
||||
|
||||
-
|
||||
id: 8
|
||||
index: 2
|
||||
repo_id: 62
|
||||
state: "error"
|
||||
sha: "774f93df12d14931ea93259ae93418da4482fcc1"
|
||||
target_url: "/user2/test_workflows/actions"
|
||||
description: "My awesome deploy service - v2"
|
||||
context: deploy/awesomeness
|
||||
context_hash: ae9547713a6665fc4261d0756904932085a41cf2
|
||||
creator_id: 2
|
||||
|
|
|
@ -84,6 +84,8 @@ var migrations = []*Migration{
|
|||
NewMigration("Add `legacy` to `web_authn_credential` table", AddLegacyToWebAuthnCredential),
|
||||
// v23 -> v24
|
||||
NewMigration("Add `delete_branch_after_merge` to `auto_merge` table", AddDeleteBranchAfterMergeToAutoMerge),
|
||||
// v24 -> v25
|
||||
NewMigration("Add `purpose` column to `forgejo_auth_token` table", AddPurposeToForgejoAuthToken),
|
||||
}
|
||||
|
||||
// GetCurrentDBVersion returns the current Forgejo database version.
|
||||
|
|
19
models/forgejo_migrations/v24.go
Normal file
19
models/forgejo_migrations/v24.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package forgejo_migrations //nolint:revive
|
||||
|
||||
import "xorm.io/xorm"
|
||||
|
||||
func AddPurposeToForgejoAuthToken(x *xorm.Engine) error {
|
||||
type ForgejoAuthToken struct {
|
||||
ID int64 `xorm:"pk autoincr"`
|
||||
Purpose string `xorm:"NOT NULL DEFAULT 'long_term_authorization'"`
|
||||
}
|
||||
if err := x.Sync(new(ForgejoAuthToken)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := x.Exec("UPDATE `forgejo_auth_token` SET purpose = 'long_term_authorization' WHERE purpose = ''")
|
||||
return err
|
||||
}
|
|
@ -288,27 +288,18 @@ func GetLatestCommitStatus(ctx context.Context, repoID int64, sha string, listOp
|
|||
|
||||
// GetLatestCommitStatusForPairs returns all statuses with a unique context for a given list of repo-sha pairs
|
||||
func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map[int64][]*CommitStatus, error) {
|
||||
type result struct {
|
||||
Index int64
|
||||
RepoID int64
|
||||
SHA string
|
||||
}
|
||||
|
||||
results := make([]result, 0, len(repoSHAs))
|
||||
|
||||
getBase := func() *xorm.Session {
|
||||
return db.GetEngine(ctx).Table(&CommitStatus{})
|
||||
}
|
||||
results := []*CommitStatus{}
|
||||
|
||||
// Create a disjunction of conditions for each repoID and SHA pair
|
||||
conds := make([]builder.Cond, 0, len(repoSHAs))
|
||||
for _, repoSHA := range repoSHAs {
|
||||
conds = append(conds, builder.Eq{"repo_id": repoSHA.RepoID, "sha": repoSHA.SHA})
|
||||
}
|
||||
sess := getBase().Where(builder.Or(conds...)).
|
||||
Select("max( `index` ) as `index`, repo_id, sha").
|
||||
GroupBy("context_hash, repo_id, sha").OrderBy("max( `index` ) desc")
|
||||
|
||||
sess := db.GetEngine(ctx).Table(&CommitStatus{}).
|
||||
Select("MAX(`index`) AS `index`, *").
|
||||
Where(builder.Or(conds...)).
|
||||
GroupBy("context_hash, repo_id, sha").OrderBy("MAX(`index`) DESC")
|
||||
err := sess.Find(&results)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -316,27 +307,9 @@ func GetLatestCommitStatusForPairs(ctx context.Context, repoSHAs []RepoSHA) (map
|
|||
|
||||
repoStatuses := make(map[int64][]*CommitStatus)
|
||||
|
||||
if len(results) > 0 {
|
||||
statuses := make([]*CommitStatus, 0, len(results))
|
||||
|
||||
conds = make([]builder.Cond, 0, len(results))
|
||||
for _, result := range results {
|
||||
cond := builder.Eq{
|
||||
"`index`": result.Index,
|
||||
"repo_id": result.RepoID,
|
||||
"sha": result.SHA,
|
||||
}
|
||||
conds = append(conds, cond)
|
||||
}
|
||||
err = getBase().Where(builder.Or(conds...)).Find(&statuses)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Group the statuses by repo ID
|
||||
for _, status := range statuses {
|
||||
repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
|
||||
}
|
||||
// Group the statuses by repo ID
|
||||
for _, status := range results {
|
||||
repoStatuses[status.RepoID] = append(repoStatuses[status.RepoID], status)
|
||||
}
|
||||
|
||||
return repoStatuses, nil
|
||||
|
|
|
@ -35,8 +35,8 @@ func TestGetCommitStatuses(t *testing.T) {
|
|||
SHA: sha1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5, int(maxResults))
|
||||
assert.Len(t, statuses, 5)
|
||||
assert.EqualValues(t, 6, maxResults)
|
||||
assert.Len(t, statuses, 6)
|
||||
|
||||
assert.Equal(t, "ci/awesomeness", statuses[0].Context)
|
||||
assert.Equal(t, structs.CommitStatusPending, statuses[0].State)
|
||||
|
@ -58,13 +58,17 @@ func TestGetCommitStatuses(t *testing.T) {
|
|||
assert.Equal(t, structs.CommitStatusError, statuses[4].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[4].APIURL(db.DefaultContext))
|
||||
|
||||
assert.Equal(t, "deploy/awesomeness", statuses[5].Context)
|
||||
assert.Equal(t, structs.CommitStatusPending, statuses[5].State)
|
||||
assert.Equal(t, "https://try.gitea.io/api/v1/repos/user2/repo1/statuses/1234123412341234123412341234123412341234", statuses[5].APIURL(db.DefaultContext))
|
||||
|
||||
statuses, maxResults, err = db.FindAndCount[git_model.CommitStatus](db.DefaultContext, &git_model.CommitStatusOptions{
|
||||
ListOptions: db.ListOptions{Page: 2, PageSize: 50},
|
||||
RepoID: repo1.ID,
|
||||
SHA: sha1,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 5, int(maxResults))
|
||||
assert.EqualValues(t, 6, maxResults)
|
||||
assert.Empty(t, statuses)
|
||||
}
|
||||
|
||||
|
@ -265,3 +269,148 @@ func TestCommitStatusesHideActionsURL(t *testing.T) {
|
|||
assert.Empty(t, statuses[0].TargetURL)
|
||||
assert.Equal(t, "https://mycicd.org/1", statuses[1].TargetURL)
|
||||
}
|
||||
|
||||
func TestGetLatestCommitStatusForPairs(t *testing.T) {
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
t.Run("All", func(t *testing.T) {
|
||||
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{
|
||||
1: {
|
||||
{
|
||||
ID: 7,
|
||||
Index: 6,
|
||||
RepoID: 1,
|
||||
State: structs.CommitStatusPending,
|
||||
SHA: "1234123412341234123412341234123412341234",
|
||||
TargetURL: "https://example.com/builds/",
|
||||
Description: "My awesome deploy service",
|
||||
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
|
||||
Context: "deploy/awesomeness",
|
||||
CreatorID: 2,
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Index: 4,
|
||||
State: structs.CommitStatusFailure,
|
||||
TargetURL: "https://example.com/builds/",
|
||||
Description: "My awesome CI-service",
|
||||
Context: "ci/awesomeness",
|
||||
CreatorID: 2,
|
||||
RepoID: 1,
|
||||
SHA: "1234123412341234123412341234123412341234",
|
||||
ContextHash: "c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7",
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Index: 3,
|
||||
State: structs.CommitStatusSuccess,
|
||||
TargetURL: "https://example.com/coverage/",
|
||||
Description: "My awesome Coverage service",
|
||||
Context: "cov/awesomeness",
|
||||
CreatorID: 2,
|
||||
RepoID: 1,
|
||||
SHA: "1234123412341234123412341234123412341234",
|
||||
ContextHash: "3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe",
|
||||
},
|
||||
},
|
||||
62: {
|
||||
{
|
||||
ID: 8,
|
||||
Index: 2,
|
||||
RepoID: 62,
|
||||
State: structs.CommitStatusError,
|
||||
TargetURL: "/user2/test_workflows/actions",
|
||||
Description: "My awesome deploy service - v2",
|
||||
Context: "deploy/awesomeness",
|
||||
SHA: "774f93df12d14931ea93259ae93418da4482fcc1",
|
||||
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
|
||||
CreatorID: 2,
|
||||
},
|
||||
},
|
||||
}, pairs)
|
||||
})
|
||||
|
||||
t.Run("Repo 1", func(t *testing.T) {
|
||||
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{1, "1234123412341234123412341234123412341234"}})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{
|
||||
1: {
|
||||
{
|
||||
ID: 7,
|
||||
Index: 6,
|
||||
RepoID: 1,
|
||||
State: structs.CommitStatusPending,
|
||||
SHA: "1234123412341234123412341234123412341234",
|
||||
TargetURL: "https://example.com/builds/",
|
||||
Description: "My awesome deploy service",
|
||||
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
|
||||
Context: "deploy/awesomeness",
|
||||
CreatorID: 2,
|
||||
},
|
||||
{
|
||||
ID: 4,
|
||||
Index: 4,
|
||||
State: structs.CommitStatusFailure,
|
||||
TargetURL: "https://example.com/builds/",
|
||||
Description: "My awesome CI-service",
|
||||
Context: "ci/awesomeness",
|
||||
CreatorID: 2,
|
||||
RepoID: 1,
|
||||
SHA: "1234123412341234123412341234123412341234",
|
||||
ContextHash: "c65f4d64a3b14a3eced0c9b36799e66e1bd5ced7",
|
||||
},
|
||||
{
|
||||
ID: 3,
|
||||
Index: 3,
|
||||
State: structs.CommitStatusSuccess,
|
||||
TargetURL: "https://example.com/coverage/",
|
||||
Description: "My awesome Coverage service",
|
||||
Context: "cov/awesomeness",
|
||||
CreatorID: 2,
|
||||
RepoID: 1,
|
||||
SHA: "1234123412341234123412341234123412341234",
|
||||
ContextHash: "3929ac7bccd3fa1bf9b38ddedb77973b1b9a8cfe",
|
||||
},
|
||||
},
|
||||
}, pairs)
|
||||
})
|
||||
t.Run("Repo 62", func(t *testing.T) {
|
||||
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{62, "774f93df12d14931ea93259ae93418da4482fcc1"}})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{
|
||||
62: {
|
||||
{
|
||||
ID: 8,
|
||||
Index: 2,
|
||||
RepoID: 62,
|
||||
State: structs.CommitStatusError,
|
||||
TargetURL: "/user2/test_workflows/actions",
|
||||
Description: "My awesome deploy service - v2",
|
||||
Context: "deploy/awesomeness",
|
||||
SHA: "774f93df12d14931ea93259ae93418da4482fcc1",
|
||||
ContextHash: "ae9547713a6665fc4261d0756904932085a41cf2",
|
||||
CreatorID: 2,
|
||||
},
|
||||
},
|
||||
}, pairs)
|
||||
})
|
||||
|
||||
t.Run("Repo 62 nonexistant sha", func(t *testing.T) {
|
||||
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{62, "774f93df12d14931ea93259ae93418da4482fcc"}})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{}, pairs)
|
||||
})
|
||||
|
||||
t.Run("SHA with non existant repo id", func(t *testing.T) {
|
||||
pairs, err := git_model.GetLatestCommitStatusForPairs(db.DefaultContext, []git_model.RepoSHA{{1, "774f93df12d14931ea93259ae93418da4482fcc1"}})
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.EqualValues(t, map[int64][]*git_model.CommitStatus{}, pairs)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
-
|
||||
id: 1001
|
||||
owner_id: 33
|
||||
owner_name: user33
|
||||
lower_name: repo1001
|
||||
name: repo1001
|
||||
default_branch: main
|
||||
num_watches: 0
|
||||
num_stars: 0
|
||||
num_forks: 0
|
||||
num_issues: 0
|
||||
num_closed_issues: 0
|
||||
num_pulls: 0
|
||||
num_closed_pulls: 0
|
||||
num_milestones: 0
|
||||
num_closed_milestones: 0
|
||||
num_projects: 0
|
||||
num_closed_projects: 0
|
||||
is_private: false
|
||||
is_empty: false
|
||||
is_archived: false
|
||||
is_mirror: false
|
||||
status: 0
|
||||
is_fork: false
|
||||
fork_id: 0
|
||||
is_template: false
|
||||
template_id: 0
|
||||
size: 0
|
||||
is_fsck_enabled: true
|
||||
close_issues_via_commit_in_any_branch: false
|
|
@ -7,6 +7,7 @@ import (
|
|||
"context"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/unit"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
@ -54,9 +55,9 @@ func GetUserFork(ctx context.Context, repoID, userID int64) (*Repository, error)
|
|||
return &forkedRepo, nil
|
||||
}
|
||||
|
||||
// GetForks returns all the forks of the repository
|
||||
func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions) ([]*Repository, error) {
|
||||
sess := db.GetEngine(ctx)
|
||||
// GetForks returns all the forks of the repository that are visible to the user.
|
||||
func GetForks(ctx context.Context, repo *Repository, user *user_model.User, listOptions db.ListOptions) ([]*Repository, int64, error) {
|
||||
sess := db.GetEngine(ctx).Where(AccessibleRepositoryCondition(user, unit.TypeInvalid))
|
||||
|
||||
var forks []*Repository
|
||||
if listOptions.Page == 0 {
|
||||
|
@ -66,7 +67,8 @@ func GetForks(ctx context.Context, repo *Repository, listOptions db.ListOptions)
|
|||
sess = db.SetSessionPagination(sess, &listOptions)
|
||||
}
|
||||
|
||||
return forks, sess.Find(&forks, &Repository{ForkID: repo.ID})
|
||||
count, err := sess.FindAndCount(&forks, &Repository{ForkID: repo.ID})
|
||||
return forks, count, err
|
||||
}
|
||||
|
||||
// IncrementRepoForkNum increment repository fork number
|
||||
|
|
|
@ -641,12 +641,9 @@ func AccessibleRepositoryCondition(user *user_model.User, unitType unit.Type) bu
|
|||
// 1. Be able to see all non-private repositories that either:
|
||||
cond = cond.Or(builder.And(
|
||||
builder.Eq{"`repository`.is_private": false},
|
||||
// 2. Aren't in an private organisation or limited organisation if we're not logged in
|
||||
// 2. Aren't in an private organisation/user or limited organisation/user if the doer is not logged in.
|
||||
builder.NotIn("`repository`.owner_id", builder.Select("id").From("`user`").Where(
|
||||
builder.And(
|
||||
builder.Eq{"type": user_model.UserTypeOrganization},
|
||||
builder.In("visibility", orgVisibilityLimit)),
|
||||
))))
|
||||
builder.In("visibility", orgVisibilityLimit)))))
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
|
|
|
@ -4,13 +4,18 @@
|
|||
package repo_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
"code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -403,3 +408,43 @@ func TestSearchRepositoryByTopicName(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSearchRepositoryIDsByCondition(t *testing.T) {
|
||||
defer unittest.OverrideFixtures(
|
||||
unittest.FixturesOptions{
|
||||
Dir: filepath.Join(setting.AppWorkPath, "models/fixtures/"),
|
||||
Base: setting.AppWorkPath,
|
||||
Dirs: []string{"models/repo/TestSearchRepositoryIDsByCondition/"},
|
||||
},
|
||||
)()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
// Sanity check of the database
|
||||
limitedUser := unittest.AssertExistsAndLoadBean(t, &user.User{ID: 33, Visibility: structs.VisibleTypeLimited})
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1001, OwnerID: limitedUser.ID})
|
||||
|
||||
testCases := []struct {
|
||||
user *user.User
|
||||
repoIDs []int64
|
||||
}{
|
||||
{
|
||||
user: nil,
|
||||
repoIDs: []int64{1, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1059},
|
||||
},
|
||||
{
|
||||
user: unittest.AssertExistsAndLoadBean(t, &user.User{ID: 4}),
|
||||
repoIDs: []int64{1, 3, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 38, 40, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1001, 1059},
|
||||
},
|
||||
{
|
||||
user: unittest.AssertExistsAndLoadBean(t, &user.User{ID: 5}),
|
||||
repoIDs: []int64{1, 4, 8, 9, 10, 11, 12, 14, 17, 18, 21, 23, 25, 27, 29, 32, 33, 34, 35, 36, 37, 38, 40, 42, 44, 45, 46, 47, 48, 49, 50, 51, 53, 57, 58, 60, 61, 62, 1001, 1059},
|
||||
},
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
repoIDs, err := repo_model.FindUserCodeAccessibleRepoIDs(db.DefaultContext, testCase.user)
|
||||
require.NoError(t, err)
|
||||
|
||||
slices.Sort(repoIDs)
|
||||
assert.EqualValues(t, testCase.repoIDs, repoIDs)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -75,26 +75,28 @@ func GetRepoAssignees(ctx context.Context, repo *Repository) (_ []*user_model.Us
|
|||
return nil, err
|
||||
}
|
||||
|
||||
additionalUserIDs := make([]int64, 0, 10)
|
||||
if err = e.Table("team_user").
|
||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
||||
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
||||
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
|
||||
Distinct("`team_user`.uid").
|
||||
Select("`team_user`.uid").
|
||||
Find(&additionalUserIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uniqueUserIDs := make(container.Set[int64])
|
||||
uniqueUserIDs.AddMultiple(userIDs...)
|
||||
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
||||
|
||||
if repo.Owner.IsOrganization() {
|
||||
additionalUserIDs := make([]int64, 0, 10)
|
||||
if err = e.Table("team_user").
|
||||
Join("INNER", "team_repo", "`team_repo`.team_id = `team_user`.team_id").
|
||||
Join("INNER", "team_unit", "`team_unit`.team_id = `team_user`.team_id").
|
||||
Where("`team_repo`.repo_id = ? AND (`team_unit`.access_mode >= ? OR (`team_unit`.access_mode = ? AND `team_unit`.`type` = ?))",
|
||||
repo.ID, perm.AccessModeWrite, perm.AccessModeRead, unit.TypePullRequests).
|
||||
Distinct("`team_user`.uid").
|
||||
Select("`team_user`.uid").
|
||||
Find(&additionalUserIDs); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
uniqueUserIDs.AddMultiple(additionalUserIDs...)
|
||||
}
|
||||
|
||||
// Leave a seat for owner itself to append later, but if owner is an organization
|
||||
// and just waste 1 unit is cheaper than re-allocate memory once.
|
||||
users := make([]*user_model.User, 0, len(uniqueUserIDs)+1)
|
||||
if len(userIDs) > 0 {
|
||||
if len(uniqueUserIDs) > 0 {
|
||||
if err = e.In("id", uniqueUserIDs.Values()).
|
||||
Where(builder.Eq{"`user`.is_active": true}).
|
||||
OrderBy(user_model.GetOrderByName()).
|
||||
|
|
|
@ -8,10 +8,8 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/modules/base"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
@ -246,23 +244,6 @@ func updateActivation(ctx context.Context, email *EmailAddress, activate bool) e
|
|||
return UpdateUserCols(ctx, user, "rands")
|
||||
}
|
||||
|
||||
// VerifyActiveEmailCode verifies active email code when active account
|
||||
func VerifyActiveEmailCode(ctx context.Context, code, email string) *EmailAddress {
|
||||
if user := GetVerifyUser(ctx, code); user != nil {
|
||||
// time limit code
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, email, user.LowerName, user.Passwd, user.Rands)
|
||||
|
||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||
emailAddress := &EmailAddress{UID: user.ID, Email: email}
|
||||
if has, _ := db.GetEngine(ctx).Get(emailAddress); has {
|
||||
return emailAddress
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SearchEmailOrderBy is used to sort the results from SearchEmails()
|
||||
type SearchEmailOrderBy string
|
||||
|
||||
|
|
|
@ -7,7 +7,9 @@ package user
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"net/url"
|
||||
|
@ -318,15 +320,14 @@ func (u *User) OrganisationLink() string {
|
|||
return setting.AppSubURL + "/org/" + url.PathEscape(u.Name)
|
||||
}
|
||||
|
||||
// GenerateEmailActivateCode generates an activate code based on user information and given e-mail.
|
||||
func (u *User) GenerateEmailActivateCode(email string) string {
|
||||
code := base.CreateTimeLimitCode(
|
||||
fmt.Sprintf("%d%s%s%s%s", u.ID, email, u.LowerName, u.Passwd, u.Rands),
|
||||
setting.Service.ActiveCodeLives, time.Now(), nil)
|
||||
|
||||
// Add tail hex username
|
||||
code += hex.EncodeToString([]byte(u.LowerName))
|
||||
return code
|
||||
// GenerateEmailAuthorizationCode generates an activation code based for the user for the specified purpose.
|
||||
// The standard expiry is ActiveCodeLives minutes.
|
||||
func (u *User) GenerateEmailAuthorizationCode(ctx context.Context, purpose auth.AuthorizationPurpose) (string, error) {
|
||||
lookup, validator, err := auth.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(setting.Service.ActiveCodeLives)*60), purpose)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return lookup + ":" + validator, nil
|
||||
}
|
||||
|
||||
// GetUserFollowers returns range of user's followers.
|
||||
|
@ -838,35 +839,50 @@ func countUsers(ctx context.Context, opts *CountUserFilter) int64 {
|
|||
return count
|
||||
}
|
||||
|
||||
// GetVerifyUser get user by verify code
|
||||
func GetVerifyUser(ctx context.Context, code string) (user *User) {
|
||||
if len(code) <= base.TimeLimitCodeLength {
|
||||
return nil
|
||||
// VerifyUserActiveCode verifies that the code is valid for the given purpose for this user.
|
||||
// If delete is specified, the token will be deleted.
|
||||
func VerifyUserAuthorizationToken(ctx context.Context, code string, purpose auth.AuthorizationPurpose, delete bool) (*User, error) {
|
||||
lookupKey, validator, found := strings.Cut(code, ":")
|
||||
if !found {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// use tail hex username query user
|
||||
hexStr := code[base.TimeLimitCodeLength:]
|
||||
if b, err := hex.DecodeString(hexStr); err == nil {
|
||||
if user, err = GetUserByName(ctx, string(b)); user != nil {
|
||||
return user
|
||||
authToken, err := auth.FindAuthToken(ctx, lookupKey, purpose)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
log.Error("user.getVerifyUser: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
if authToken.IsExpired() {
|
||||
return nil, auth.DeleteAuthToken(ctx, authToken)
|
||||
}
|
||||
|
||||
// VerifyUserActiveCode verifies active code when active account
|
||||
func VerifyUserActiveCode(ctx context.Context, code string) (user *User) {
|
||||
if user = GetVerifyUser(ctx, code); user != nil {
|
||||
// time limit code
|
||||
prefix := code[:base.TimeLimitCodeLength]
|
||||
data := fmt.Sprintf("%d%s%s%s%s", user.ID, user.Email, user.LowerName, user.Passwd, user.Rands)
|
||||
if base.VerifyTimeLimitCode(time.Now(), data, setting.Service.ActiveCodeLives, prefix) {
|
||||
return user
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
|
||||
return nil, errors.New("validator doesn't match")
|
||||
}
|
||||
|
||||
u, err := GetUserByID(ctx, authToken.UID)
|
||||
if err != nil {
|
||||
if IsErrUserNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if delete {
|
||||
if err := auth.DeleteAuthToken(ctx, authToken); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// ValidateUser check if user is valid to insert / update into database
|
||||
|
|
|
@ -7,6 +7,7 @@ package user_test
|
|||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
@ -21,7 +22,9 @@ import (
|
|||
"code.gitea.io/gitea/modules/optional"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/modules/timeutil"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/modules/validation"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
|
@ -700,3 +703,66 @@ func TestDisabledUserFeatures(t *testing.T) {
|
|||
assert.True(t, user_model.IsFeatureDisabledWithLoginType(user, f))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateEmailAuthorizationCode(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(code, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth.HashValidator(rawValidator))
|
||||
|
||||
authToken.Expiry = authToken.Expiry.Add(-int64(setting.Service.ActiveCodeLives) * 60)
|
||||
assert.True(t, authToken.IsExpired())
|
||||
}
|
||||
|
||||
func TestVerifyUserAuthorizationToken(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 2)()
|
||||
require.NoError(t, unittest.PrepareTestDatabase())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, _, ok := strings.Cut(code, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
t.Run("Wrong purpose", func(t *testing.T) {
|
||||
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.PasswordReset, false)
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, u)
|
||||
})
|
||||
|
||||
t.Run("No delete", func(t *testing.T) {
|
||||
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, false)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, user.ID, u.ID)
|
||||
|
||||
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, authToken)
|
||||
})
|
||||
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
u, err := user_model.VerifyUserAuthorizationToken(db.DefaultContext, code, auth.UserActivation, true)
|
||||
require.NoError(t, err)
|
||||
assert.EqualValues(t, user.ID, u.ID)
|
||||
|
||||
authToken, err := auth.FindAuthToken(db.DefaultContext, lookupKey, auth.UserActivation)
|
||||
require.ErrorIs(t, err, util.ErrNotExist)
|
||||
assert.Nil(t, authToken)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,26 +4,20 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
|
||||
"github.com/dustin/go-humanize"
|
||||
)
|
||||
|
@ -54,66 +48,6 @@ func BasicAuthDecode(encoded string) (string, string, error) {
|
|||
return "", "", errors.New("invalid basic authentication")
|
||||
}
|
||||
|
||||
// VerifyTimeLimitCode verify time limit code
|
||||
func VerifyTimeLimitCode(now time.Time, data string, minutes int, code string) bool {
|
||||
if len(code) <= 18 {
|
||||
return false
|
||||
}
|
||||
|
||||
startTimeStr := code[:12]
|
||||
aliveTimeStr := code[12:18]
|
||||
aliveTime, _ := strconv.Atoi(aliveTimeStr) // no need to check err, if anything wrong, the following code check will fail soon
|
||||
|
||||
// check code
|
||||
retCode := CreateTimeLimitCode(data, aliveTime, startTimeStr, nil)
|
||||
if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
|
||||
retCode = CreateTimeLimitCode(data, aliveTime, startTimeStr, sha1.New()) // TODO: this is only for the support of legacy codes, remove this in/after 1.23
|
||||
if subtle.ConstantTimeCompare([]byte(retCode), []byte(code)) != 1 {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// check time is expired or not: startTime <= now && now < startTime + minutes
|
||||
startTime, _ := time.ParseInLocation("200601021504", startTimeStr, time.Local)
|
||||
return (startTime.Before(now) || startTime.Equal(now)) && now.Before(startTime.Add(time.Minute*time.Duration(minutes)))
|
||||
}
|
||||
|
||||
// TimeLimitCodeLength default value for time limit code
|
||||
const TimeLimitCodeLength = 12 + 6 + 40
|
||||
|
||||
// CreateTimeLimitCode create a time-limited code.
|
||||
// Format: 12 length date time string + 6 minutes string (not used) + 40 hash string, some other code depends on this fixed length
|
||||
// If h is nil, then use the default hmac hash.
|
||||
func CreateTimeLimitCode[T time.Time | string](data string, minutes int, startTimeGeneric T, h hash.Hash) string {
|
||||
const format = "200601021504"
|
||||
|
||||
var start time.Time
|
||||
var startTimeAny any = startTimeGeneric
|
||||
if t, ok := startTimeAny.(time.Time); ok {
|
||||
start = t
|
||||
} else {
|
||||
var err error
|
||||
start, err = time.ParseInLocation(format, startTimeAny.(string), time.Local)
|
||||
if err != nil {
|
||||
return "" // return an invalid code because the "parse" failed
|
||||
}
|
||||
}
|
||||
startStr := start.Format(format)
|
||||
end := start.Add(time.Minute * time.Duration(minutes))
|
||||
|
||||
if h == nil {
|
||||
h = hmac.New(sha1.New, setting.GetGeneralTokenSigningSecret())
|
||||
}
|
||||
_, _ = fmt.Fprintf(h, "%s%s%s%s%d", data, hex.EncodeToString(setting.GetGeneralTokenSigningSecret()), startStr, end.Format(format), minutes)
|
||||
encoded := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
code := fmt.Sprintf("%s%06d%s", startStr, minutes, encoded)
|
||||
if len(code) != TimeLimitCodeLength {
|
||||
panic("there is a hard requirement for the length of time-limited code") // it shouldn't happen
|
||||
}
|
||||
return code
|
||||
}
|
||||
|
||||
// FileSize calculates the file size and generate user-friendly string.
|
||||
func FileSize(s int64) string {
|
||||
return humanize.IBytes(uint64(s))
|
||||
|
|
|
@ -4,13 +4,7 @@
|
|||
package base
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -46,57 +40,6 @@ func TestBasicAuthDecode(t *testing.T) {
|
|||
require.Error(t, err)
|
||||
}
|
||||
|
||||
func TestVerifyTimeLimitCode(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.InstallLock, true)()
|
||||
initGeneralSecret := func(secret string) {
|
||||
setting.InstallLock = true
|
||||
setting.CfgProvider, _ = setting.NewConfigProviderFromData(fmt.Sprintf(`
|
||||
[oauth2]
|
||||
JWT_SECRET = %s
|
||||
`, secret))
|
||||
setting.LoadCommonSettings()
|
||||
}
|
||||
|
||||
initGeneralSecret("KZb_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
|
||||
now := time.Now()
|
||||
|
||||
t.Run("TestGenericParameter", func(t *testing.T) {
|
||||
time2000 := time.Date(2000, 1, 2, 3, 4, 5, 0, time.Local)
|
||||
assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, time2000, sha1.New()))
|
||||
assert.Equal(t, "2000010203040000026fa5221b2731b7cf80b1b506f5e39e38c115fee5", CreateTimeLimitCode("test-sha1", 2, "200001020304", sha1.New()))
|
||||
assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, time2000, nil))
|
||||
assert.Equal(t, "2000010203040000024842227a2f87041ff82025199c0187410a9297bf", CreateTimeLimitCode("test-hmac", 2, "200001020304", nil))
|
||||
})
|
||||
|
||||
t.Run("TestInvalidCode", func(t *testing.T) {
|
||||
assert.False(t, VerifyTimeLimitCode(now, "data", 2, ""))
|
||||
assert.False(t, VerifyTimeLimitCode(now, "data", 2, "invalid code"))
|
||||
})
|
||||
|
||||
t.Run("TestCreateAndVerify", func(t *testing.T) {
|
||||
code := CreateTimeLimitCode("data", 2, now, nil)
|
||||
assert.False(t, VerifyTimeLimitCode(now.Add(-time.Minute), "data", 2, code)) // not started yet
|
||||
assert.True(t, VerifyTimeLimitCode(now, "data", 2, code))
|
||||
assert.True(t, VerifyTimeLimitCode(now.Add(time.Minute), "data", 2, code))
|
||||
assert.False(t, VerifyTimeLimitCode(now.Add(time.Minute), "DATA", 2, code)) // invalid data
|
||||
assert.False(t, VerifyTimeLimitCode(now.Add(2*time.Minute), "data", 2, code)) // expired
|
||||
})
|
||||
|
||||
t.Run("TestDifferentSecret", func(t *testing.T) {
|
||||
// use another secret to ensure the code is invalid for different secret
|
||||
verifyDataCode := func(c string) bool {
|
||||
return VerifyTimeLimitCode(now, "data", 2, c)
|
||||
}
|
||||
code1 := CreateTimeLimitCode("data", 2, now, sha1.New())
|
||||
code2 := CreateTimeLimitCode("data", 2, now, nil)
|
||||
assert.True(t, verifyDataCode(code1))
|
||||
assert.True(t, verifyDataCode(code2))
|
||||
initGeneralSecret("000_QLUd4fYVyxetjxC4eZkrBgWM2SndOOWDNtgUUko")
|
||||
assert.False(t, verifyDataCode(code1))
|
||||
assert.False(t, verifyDataCode(code2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileSize(t *testing.T) {
|
||||
var size int64 = 512
|
||||
assert.Equal(t, "512 B", FileSize(size))
|
||||
|
|
|
@ -97,7 +97,7 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(ref-issue( ref-external-issue)?|mention)$`)).OnElements("a")
|
||||
|
||||
// Allow classes for task lists
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`task-list-item`)).OnElements("li")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^task-list-item$`)).OnElements("li")
|
||||
|
||||
// Allow classes for org mode list item status.
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^(unchecked|checked|indeterminate)$`)).OnElements("li")
|
||||
|
@ -106,7 +106,7 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^icon(\s+[\p{L}\p{N}_-]+)+$`)).OnElements("i")
|
||||
|
||||
// Allow classes for emojis
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`emoji`)).OnElements("img")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^emoji$`)).OnElements("img")
|
||||
|
||||
// Allow icons, emojis, chroma syntax and keyword markup on span
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile(`^((icon(\s+[\p{L}\p{N}_-]+)+)|(emoji)|(language-math display)|(language-math inline))$|^([a-z][a-z0-9]{0,2})$|^` + keywordClass + `$`)).OnElements("span")
|
||||
|
@ -123,13 +123,13 @@ func createDefaultPolicy() *bluemonday.Policy {
|
|||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^header$")).OnElements("div")
|
||||
policy.AllowAttrs("data-line-number").Matching(regexp.MustCompile("^[0-9]+$")).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^text small grey$")).OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview*")).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^file-preview$")).OnElements("table")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^lines-escape$")).OnElements("td")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^toggle-escape-button btn interact-bg$")).OnElements("button")
|
||||
policy.AllowAttrs("title").OnElements("button")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ambiguous-code-point$")).OnElements("span")
|
||||
policy.AllowAttrs("data-tooltip-content").OnElements("span")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("muted|(text black)")).OnElements("a")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^muted|(text black)$")).OnElements("a")
|
||||
policy.AllowAttrs("class").Matching(regexp.MustCompile("^ui warning message tw-text-left$")).OnElements("div")
|
||||
|
||||
// Allow generally safe attributes
|
||||
|
|
30
package-lock.json
generated
30
package-lock.json
generated
|
@ -43,7 +43,7 @@
|
|||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.3",
|
||||
"swagger-ui-dist": "5.17.14",
|
||||
"tailwindcss": "3.4.13",
|
||||
"tailwindcss": "3.4.15",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tippy.js": "6.3.7",
|
||||
|
@ -15389,33 +15389,33 @@
|
|||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.13",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz",
|
||||
"integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==",
|
||||
"version": "3.4.15",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.15.tgz",
|
||||
"integrity": "sha512-r4MeXnfBmSOuKUWmXe6h2CcyfzJCEk4F0pptO5jlnYSIViUkVmsawj80N5h2lO3gwcmSb4n3PuN+e+GC1Guylw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
"chokidar": "^3.5.3",
|
||||
"chokidar": "^3.6.0",
|
||||
"didyoumean": "^1.2.2",
|
||||
"dlv": "^1.1.3",
|
||||
"fast-glob": "^3.3.0",
|
||||
"fast-glob": "^3.3.2",
|
||||
"glob-parent": "^6.0.2",
|
||||
"is-glob": "^4.0.3",
|
||||
"jiti": "^1.21.0",
|
||||
"jiti": "^1.21.6",
|
||||
"lilconfig": "^2.1.0",
|
||||
"micromatch": "^4.0.5",
|
||||
"micromatch": "^4.0.8",
|
||||
"normalize-path": "^3.0.0",
|
||||
"object-hash": "^3.0.0",
|
||||
"picocolors": "^1.0.0",
|
||||
"postcss": "^8.4.23",
|
||||
"picocolors": "^1.1.1",
|
||||
"postcss": "^8.4.47",
|
||||
"postcss-import": "^15.1.0",
|
||||
"postcss-js": "^4.0.1",
|
||||
"postcss-load-config": "^4.0.1",
|
||||
"postcss-nested": "^6.0.1",
|
||||
"postcss-selector-parser": "^6.0.11",
|
||||
"resolve": "^1.22.2",
|
||||
"sucrase": "^3.32.0"
|
||||
"postcss-load-config": "^4.0.2",
|
||||
"postcss-nested": "^6.2.0",
|
||||
"postcss-selector-parser": "^6.1.2",
|
||||
"resolve": "^1.22.8",
|
||||
"sucrase": "^3.35.0"
|
||||
},
|
||||
"bin": {
|
||||
"tailwind": "lib/cli.js",
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
"pretty-ms": "9.0.0",
|
||||
"sortablejs": "1.15.3",
|
||||
"swagger-ui-dist": "5.17.14",
|
||||
"tailwindcss": "3.4.13",
|
||||
"tailwindcss": "3.4.15",
|
||||
"throttle-debounce": "5.0.0",
|
||||
"tinycolor2": "1.6.0",
|
||||
"tippy.js": "6.3.7",
|
||||
|
|
8
release-notes/5974.md
Normal file
8
release-notes/5974.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/1ce33aa38d1d258d14523ff2c7c2dbf339f22b74) it was possible to use a token sent via email for secondary email validation to reset the password instead. In other words, a token sent for a given action (registration, password reset or secondary email validation) could be used to perform a different action. It is no longer possible to use a token for an action that is different from its original purpose.
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/061abe60045212acf8c3f5c49b5cc758b4cbcde9) a fork of a public repository would show in the list of forks, even if its owner was not a public user or organization. Such a fork is now hidden from the list of forks of the public repository.
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/3e3ef76808100cb1c853378733d0f6a910324ac6) the members of an organization team with read access to a repository (e.g. to read issues) but no read access to the code could read the RSS or atom feeds which include the commit activity. Reading the RSS or atom feeds is now denied unless the team has read permissions on the code.
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/9508aa7713632ed40124a933d91d5766cf2369c2) the tokens used when [replying by email to issues or pull requests](https://forgejo.org/docs/v9.0/user/incoming/) were weaker than the [rfc2104 recommendations](https://datatracker.ietf.org/doc/html/rfc2104#section-5). The tokens are now truncated to 128 bits instead of 80 bits. It is no longer possible to reply to emails sent before the upgrade because the weaker tokens are invalid.
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/786dfc7fb81ee76d4292ca5fcb33e6ea7bdccc29) a registered user could modify the update frequency of any push mirror (e.g. every 4h instead of every 8h). They are now only able to do that if they have administrative permissions on the repository.
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/e6bbecb02d47730d3cc630d419fe27ef2fb5cb39) it was possible to use basic authorization (i.e. user:password) for requests to the API even when security keys were enrolled for a user. It is no longer possible, an application token must be used instead.
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/7067cc7da4f144cc8a2fd2ae6e5307e0465ace7f) some markup sanitation rules were not as strong as they could be (e.g. allowing `emoji somethingelse` as well as `emoji`). The rules are now stricter and do not allow for such cases.
|
||||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/b70196653f9d7d3b9d4e72d114e5cc6f472988c4) when Forgejo is configured to enable instance wide search (e.g. with [bleve](https://blevesearch.com/)), results found in the repositories of private or limited users were displayed to anonymous visitors. The results found in private or limited organizations were not displayed. The search results found in the repositories of private or limited user are no longer displayed to anonymous visitors.
|
1
release-notes/5988.md
Normal file
1
release-notes/5988.md
Normal file
|
@ -0,0 +1 @@
|
|||
fix: [commit](https://codeberg.org/forgejo/forgejo/commit/fc26becba4b08877a726f2e7e453992310245fe5) when a tag was removed and a release existed for that tag, it would be broken. The release is no longer broken the tag can be added again.
|
|
@ -56,7 +56,7 @@ func ListForks(ctx *context.APIContext) {
|
|||
// "404":
|
||||
// "$ref": "#/responses/notFound"
|
||||
|
||||
forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, utils.GetListOptions(ctx))
|
||||
forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, utils.GetListOptions(ctx))
|
||||
if err != nil {
|
||||
ctx.Error(http.StatusInternalServerError, "GetForks", err)
|
||||
return
|
||||
|
@ -71,7 +71,7 @@ func ListForks(ctx *context.APIContext) {
|
|||
apiForks[i] = convert.ToRepo(ctx, fork, permission)
|
||||
}
|
||||
|
||||
ctx.SetTotalCountHeader(int64(ctx.Repo.Repository.NumForks))
|
||||
ctx.SetTotalCountHeader(total)
|
||||
ctx.JSON(http.StatusOK, apiForks)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -63,38 +61,11 @@ func autoSignIn(ctx *context.Context) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
|
||||
lookupKey, validator, found := strings.Cut(authCookie, ":")
|
||||
if !found {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
authToken, err := auth.FindAuthToken(ctx, lookupKey)
|
||||
u, err := user_model.VerifyUserAuthorizationToken(ctx, authCookie, auth.LongTermAuthorization, false)
|
||||
if err != nil {
|
||||
if errors.Is(err, util.ErrNotExist) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
return false, fmt.Errorf("VerifyUserAuthorizationToken: %w", err)
|
||||
}
|
||||
|
||||
if authToken.IsExpired() {
|
||||
err = auth.DeleteAuthToken(ctx, authToken)
|
||||
return false, err
|
||||
}
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if subtle.ConstantTimeCompare([]byte(authToken.HashedValidator), []byte(auth.HashValidator(rawValidator))) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
u, err := user_model.GetUserByID(ctx, authToken.UID)
|
||||
if err != nil {
|
||||
if !user_model.IsErrUserNotExist(err) {
|
||||
return false, fmt.Errorf("GetUserByID: %w", err)
|
||||
}
|
||||
if u == nil {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -633,7 +604,10 @@ func handleUserCreated(ctx *context.Context, u *user_model.User, gothUser *goth.
|
|||
return false
|
||||
}
|
||||
|
||||
mailer.SendActivateAccountMail(ctx.Locale, u)
|
||||
if err := mailer.SendActivateAccountMail(ctx, u); err != nil {
|
||||
ctx.ServerError("SendActivateAccountMail", err)
|
||||
return false
|
||||
}
|
||||
|
||||
ctx.Data["IsSendRegisterMail"] = true
|
||||
ctx.Data["Email"] = u.Email
|
||||
|
@ -674,7 +648,10 @@ func Activate(ctx *context.Context) {
|
|||
ctx.Data["ResendLimited"] = true
|
||||
} else {
|
||||
ctx.Data["ActiveCodeLives"] = timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, ctx.Locale)
|
||||
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
|
||||
if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil {
|
||||
ctx.ServerError("SendActivateAccountMail", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctx.Cache.Put(cacheKey+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||
|
@ -687,7 +664,12 @@ func Activate(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
||||
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, false)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return
|
||||
}
|
||||
|
||||
// if code is wrong
|
||||
if user == nil {
|
||||
ctx.Data["IsCodeInvalid"] = true
|
||||
|
@ -751,7 +733,12 @@ func ActivatePost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
user := user_model.VerifyUserActiveCode(ctx, code)
|
||||
user, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.UserActivation, true)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return
|
||||
}
|
||||
|
||||
// if code is wrong
|
||||
if user == nil {
|
||||
ctx.Data["IsCodeInvalid"] = true
|
||||
|
@ -835,23 +822,32 @@ func ActivateEmail(ctx *context.Context) {
|
|||
code := ctx.FormString("code")
|
||||
emailStr := ctx.FormString("email")
|
||||
|
||||
// Verify code.
|
||||
if email := user_model.VerifyActiveEmailCode(ctx, code, emailStr); email != nil {
|
||||
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||
ctx.ServerError("ActivateEmail", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Email activated: %s", email.Email)
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
|
||||
|
||||
if u, err := user_model.GetUserByID(ctx, email.UID); err != nil {
|
||||
log.Warn("GetUserByID: %d", email.UID)
|
||||
} else {
|
||||
// Allow user to validate more emails
|
||||
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
|
||||
}
|
||||
u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.EmailActivation(emailStr), true)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return
|
||||
}
|
||||
if u == nil {
|
||||
ctx.Redirect(setting.AppSubURL + "/user/settings/account")
|
||||
return
|
||||
}
|
||||
|
||||
email, err := user_model.GetEmailAddressOfUser(ctx, emailStr, u.ID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetEmailAddressOfUser", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err := user_model.ActivateEmail(ctx, email); err != nil {
|
||||
ctx.ServerError("ActivateEmail", err)
|
||||
return
|
||||
}
|
||||
|
||||
log.Trace("Email activated: %s", email.Email)
|
||||
ctx.Flash.Success(ctx.Tr("settings.add_email_success"))
|
||||
|
||||
// Allow user to validate more emails
|
||||
_ = ctx.Cache.Delete("MailResendLimit_" + u.LowerName)
|
||||
|
||||
// FIXME: e-mail verification does not require the user to be logged in,
|
||||
// so this could be redirecting to the login page.
|
||||
|
|
|
@ -86,7 +86,10 @@ func ForgotPasswdPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
mailer.SendResetPasswordMail(u)
|
||||
if err := mailer.SendResetPasswordMail(ctx, u); err != nil {
|
||||
ctx.ServerError("SendResetPasswordMail", err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = ctx.Cache.Put("MailResendLimit_"+u.LowerName, u.LowerName, 180); err != nil {
|
||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||
|
@ -97,7 +100,7 @@ func ForgotPasswdPost(ctx *context.Context) {
|
|||
ctx.HTML(http.StatusOK, tplForgotPassword)
|
||||
}
|
||||
|
||||
func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFactor) {
|
||||
func commonResetPassword(ctx *context.Context, shouldDeleteToken bool) (*user_model.User, *auth.TwoFactor) {
|
||||
code := ctx.FormString("code")
|
||||
|
||||
ctx.Data["Title"] = ctx.Tr("auth.reset_password")
|
||||
|
@ -113,7 +116,12 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
|
|||
}
|
||||
|
||||
// Fail early, don't frustrate the user
|
||||
u := user_model.VerifyUserActiveCode(ctx, code)
|
||||
u, err := user_model.VerifyUserAuthorizationToken(ctx, code, auth.PasswordReset, shouldDeleteToken)
|
||||
if err != nil {
|
||||
ctx.ServerError("VerifyUserAuthorizationToken", err)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if u == nil {
|
||||
ctx.Flash.Error(ctx.Tr("auth.invalid_code_forgot_password", fmt.Sprintf("%s/user/forgot_password", setting.AppSubURL)), true)
|
||||
return nil, nil
|
||||
|
@ -145,7 +153,7 @@ func commonResetPassword(ctx *context.Context) (*user_model.User, *auth.TwoFacto
|
|||
func ResetPasswd(ctx *context.Context) {
|
||||
ctx.Data["IsResetForm"] = true
|
||||
|
||||
commonResetPassword(ctx)
|
||||
commonResetPassword(ctx, false)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
@ -155,7 +163,7 @@ func ResetPasswd(ctx *context.Context) {
|
|||
|
||||
// ResetPasswdPost response from account recovery request
|
||||
func ResetPasswdPost(ctx *context.Context) {
|
||||
u, twofa := commonResetPassword(ctx)
|
||||
u, twofa := commonResetPassword(ctx, true)
|
||||
if ctx.Written() {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -566,21 +566,19 @@ func SettingsPost(ctx *context.Context) {
|
|||
// as an error on the UI for this action
|
||||
ctx.Data["Err_RepoName"] = nil
|
||||
|
||||
m, err := selectPushMirrorByForm(ctx, form, repo)
|
||||
if err != nil {
|
||||
ctx.NotFound("", nil)
|
||||
return
|
||||
}
|
||||
|
||||
interval, err := time.ParseDuration(form.PushMirrorInterval)
|
||||
if err != nil || (interval != 0 && interval < setting.Mirror.MinInterval) {
|
||||
ctx.RenderWithErr(ctx.Tr("repo.mirror_interval_invalid"), tplSettingsOptions, &forms.RepoSettingForm{})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(form.PushMirrorID, 10, 64)
|
||||
if err != nil {
|
||||
ctx.ServerError("UpdatePushMirrorIntervalPushMirrorID", err)
|
||||
return
|
||||
}
|
||||
m := &repo_model.PushMirror{
|
||||
ID: id,
|
||||
Interval: interval,
|
||||
}
|
||||
m.Interval = interval
|
||||
if err := repo_model.UpdatePushMirrorInterval(ctx, m); err != nil {
|
||||
ctx.ServerError("UpdatePushMirrorInterval", err)
|
||||
return
|
||||
|
|
|
@ -1232,11 +1232,8 @@ func Forks(ctx *context.Context) {
|
|||
page = 1
|
||||
}
|
||||
|
||||
pager := context.NewPagination(ctx.Repo.Repository.NumForks, setting.MaxForksPerPage, page, 5)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
forks, err := repo_model.GetForks(ctx, ctx.Repo.Repository, db.ListOptions{
|
||||
Page: pager.Paginater.Current(),
|
||||
forks, total, err := repo_model.GetForks(ctx, ctx.Repo.Repository, ctx.Doer, db.ListOptions{
|
||||
Page: page,
|
||||
PageSize: setting.MaxForksPerPage,
|
||||
})
|
||||
if err != nil {
|
||||
|
@ -1244,6 +1241,9 @@ func Forks(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
pager := context.NewPagination(int(total), setting.MaxForksPerPage, page, 5)
|
||||
ctx.Data["Page"] = pager
|
||||
|
||||
for _, fork := range forks {
|
||||
if err = fork.LoadOwner(ctx); err != nil {
|
||||
ctx.ServerError("LoadOwner", err)
|
||||
|
|
|
@ -155,9 +155,15 @@ func EmailPost(ctx *context.Context) {
|
|||
return
|
||||
}
|
||||
// Only fired when the primary email is inactive (Wrong state)
|
||||
mailer.SendActivateAccountMail(ctx.Locale, ctx.Doer)
|
||||
if err := mailer.SendActivateAccountMail(ctx, ctx.Doer); err != nil {
|
||||
ctx.ServerError("SendActivateAccountMail", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
mailer.SendActivateEmailMail(ctx.Doer, email.Email)
|
||||
if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, email.Email); err != nil {
|
||||
ctx.ServerError("SendActivateEmailMail", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
address = email.Email
|
||||
|
||||
|
@ -218,7 +224,10 @@ func EmailPost(ctx *context.Context) {
|
|||
|
||||
// Send confirmation email
|
||||
if setting.Service.RegisterEmailConfirm {
|
||||
mailer.SendActivateEmailMail(ctx.Doer, form.Email)
|
||||
if err := mailer.SendActivateEmailMail(ctx, ctx.Doer, form.Email); err != nil {
|
||||
ctx.ServerError("SendActivateEmailMail", err)
|
||||
return
|
||||
}
|
||||
if err := ctx.Cache.Put("MailResendLimit_"+ctx.Doer.LowerName, ctx.Doer.LowerName, 180); err != nil {
|
||||
log.Error("Set cache(MailResendLimit) fail: %v", err)
|
||||
}
|
||||
|
|
|
@ -1562,8 +1562,10 @@ func registerRoutes(m *web.Route) {
|
|||
m.Get("/cherry-pick/{sha:([a-f0-9]{4,64})$}", repo.SetEditorconfigIfExists, repo.CherryPick)
|
||||
}, repo.MustBeNotEmpty, context.RepoRef(), reqRepoCodeReader)
|
||||
|
||||
m.Get("/rss/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("rss"))
|
||||
m.Get("/atom/branch/*", repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), feedEnabled, feed.RenderBranchFeed("atom"))
|
||||
m.Group("", func() {
|
||||
m.Get("/rss/branch/*", feed.RenderBranchFeed("rss"))
|
||||
m.Get("/atom/branch/*", feed.RenderBranchFeed("atom"))
|
||||
}, repo.MustBeNotEmpty, context.RepoRefByType(context.RepoRefBranch), reqRepoCodeReader, feedEnabled)
|
||||
|
||||
m.Group("/src", func() {
|
||||
m.Get("/branch/*", context.RepoRefByType(context.RepoRefBranch), repo.Home)
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
|
@ -132,6 +133,16 @@ func (b *Basic) Verify(req *http.Request, w http.ResponseWriter, store DataStore
|
|||
return nil, err
|
||||
}
|
||||
|
||||
hashWebAuthn, err := auth_model.HasWebAuthnRegistrationsByUID(req.Context(), u.ID)
|
||||
if err != nil {
|
||||
log.Error("HasWebAuthnRegistrationsByUID: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if hashWebAuthn {
|
||||
return nil, errors.New("Basic authorization is not allowed while having security keys enrolled")
|
||||
}
|
||||
|
||||
if skipper, ok := source.Cfg.(LocalTwoFASkipper); !ok || !skipper.IsSkipLocalTwoFA() {
|
||||
if err := validateTOTP(req, u); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -347,12 +347,30 @@ func loadOrCreateAsymmetricKey() (any, error) {
|
|||
key, err := func() (any, error) {
|
||||
switch {
|
||||
case strings.HasPrefix(setting.OAuth2.JWTSigningAlgorithm, "RS"):
|
||||
return rsa.GenerateKey(rand.Reader, 4096)
|
||||
var bits int
|
||||
switch setting.OAuth2.JWTSigningAlgorithm {
|
||||
case "RS256":
|
||||
bits = 2048
|
||||
case "RS384":
|
||||
bits = 3072
|
||||
case "RS512":
|
||||
bits = 4096
|
||||
}
|
||||
return rsa.GenerateKey(rand.Reader, bits)
|
||||
case setting.OAuth2.JWTSigningAlgorithm == "EdDSA":
|
||||
_, pk, err := ed25519.GenerateKey(rand.Reader)
|
||||
return pk, err
|
||||
default:
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
var curve elliptic.Curve
|
||||
switch setting.OAuth2.JWTSigningAlgorithm {
|
||||
case "ES256":
|
||||
curve = elliptic.P256()
|
||||
case "ES384":
|
||||
curve = elliptic.P384()
|
||||
case "ES512":
|
||||
curve = elliptic.P521()
|
||||
}
|
||||
return ecdsa.GenerateKey(curve, rand.Reader)
|
||||
}
|
||||
}()
|
||||
if err != nil {
|
||||
|
|
116
services/auth/source/oauth2/jwtsigningkey_test.go
Normal file
116
services/auth/source/oauth2/jwtsigningkey_test.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package oauth2
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLoadOrCreateAsymmetricKey(t *testing.T) {
|
||||
loadKey := func(t *testing.T) any {
|
||||
t.Helper()
|
||||
loadOrCreateAsymmetricKey()
|
||||
|
||||
fileContent, err := os.ReadFile(setting.OAuth2.JWTSigningPrivateKeyFile)
|
||||
require.NoError(t, err)
|
||||
|
||||
block, _ := pem.Decode(fileContent)
|
||||
assert.NotNil(t, block)
|
||||
assert.EqualValues(t, "PRIVATE KEY", block.Type)
|
||||
|
||||
parsedKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
require.NoError(t, err)
|
||||
|
||||
return parsedKey
|
||||
}
|
||||
t.Run("RSA-2048", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-rsa-2048.priv"))()
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "RS256")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
|
||||
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
|
||||
assert.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
|
||||
|
||||
t.Run("Load key with differ specified algorithm", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "EdDSA")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
|
||||
assert.EqualValues(t, 2048, rsaPrivateKey.N.BitLen())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("RSA-3072", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-rsa-3072.priv"))()
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "RS384")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
|
||||
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
|
||||
assert.EqualValues(t, 3072, rsaPrivateKey.N.BitLen())
|
||||
})
|
||||
|
||||
t.Run("RSA-4096", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-rsa-4096.priv"))()
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "RS512")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
|
||||
rsaPrivateKey := parsedKey.(*rsa.PrivateKey)
|
||||
assert.EqualValues(t, 4096, rsaPrivateKey.N.BitLen())
|
||||
})
|
||||
|
||||
t.Run("ECDSA-256", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-ecdsa-256.priv"))()
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "ES256")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
|
||||
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
|
||||
assert.EqualValues(t, 256, ecdsaPrivateKey.Params().BitSize)
|
||||
})
|
||||
|
||||
t.Run("ECDSA-384", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-ecdsa-384.priv"))()
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "ES384")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
|
||||
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
|
||||
assert.EqualValues(t, 384, ecdsaPrivateKey.Params().BitSize)
|
||||
})
|
||||
|
||||
t.Run("ECDSA-512", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-ecdsa-512.priv"))()
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "ES512")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
|
||||
ecdsaPrivateKey := parsedKey.(*ecdsa.PrivateKey)
|
||||
assert.EqualValues(t, 521, ecdsaPrivateKey.Params().BitSize)
|
||||
})
|
||||
|
||||
t.Run("EdDSA", func(t *testing.T) {
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningPrivateKeyFile, filepath.Join(t.TempDir(), "jwt-eddsa.priv"))()
|
||||
defer test.MockVariableValue(&setting.OAuth2.JWTSigningAlgorithm, "EdDSA")()
|
||||
|
||||
parsedKey := loadKey(t)
|
||||
|
||||
assert.NotNil(t, parsedKey.(ed25519.PrivateKey))
|
||||
})
|
||||
}
|
|
@ -47,7 +47,7 @@ func (ctx *Context) GetSiteCookie(name string) string {
|
|||
// SetLTACookie will generate a LTA token and add it as an cookie.
|
||||
func (ctx *Context) SetLTACookie(u *user_model.User) error {
|
||||
days := 86400 * setting.LogInRememberDays
|
||||
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)))
|
||||
lookup, validator, err := auth_model.GenerateAuthToken(ctx, u.ID, timeutil.TimeStampNow().Add(int64(days)), auth_model.LongTermAuthorization)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -243,6 +243,9 @@ func checkDBConsistency(ctx context.Context, logger log.Logger, autofix bool) er
|
|||
// find archive download count without existing release
|
||||
genericOrphanCheck("Archive download count without existing Release",
|
||||
"repo_archive_download_count", "release", "repo_archive_download_count.release_id=release.id"),
|
||||
// find authorization tokens without existing user
|
||||
genericOrphanCheck("Authorization token without existing User",
|
||||
"forgejo_auth_token", "user", "forgejo_auth_token.uid=user.id"),
|
||||
)
|
||||
|
||||
for _, c := range consistencyChecks {
|
||||
|
|
|
@ -70,7 +70,7 @@ func SendTestMail(email string) error {
|
|||
}
|
||||
|
||||
// sendUserMail sends a mail to the user
|
||||
func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) {
|
||||
func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, subject, info string) error {
|
||||
locale := translation.NewLocale(language)
|
||||
data := map[string]any{
|
||||
"locale": locale,
|
||||
|
@ -84,47 +84,66 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s
|
|||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
msg := NewMessage(u.EmailTo(), subject, content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)
|
||||
|
||||
SendAsync(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendActivateAccountMail sends an activation mail to the user (new user registration)
|
||||
func SendActivateAccountMail(locale translation.Locale, u *user_model.User) {
|
||||
func SendActivateAccountMail(ctx context.Context, u *user_model.User) error {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
return nil
|
||||
}
|
||||
sendUserMail(locale.Language(), u, mailAuthActivate, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.activate_account"), "activate account")
|
||||
|
||||
locale := translation.NewLocale(u.Language)
|
||||
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.UserActivation)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sendUserMail(locale.Language(), u, mailAuthActivate, code, locale.TrString("mail.activate_account"), "activate account")
|
||||
}
|
||||
|
||||
// SendResetPasswordMail sends a password reset mail to the user
|
||||
func SendResetPasswordMail(u *user_model.User) {
|
||||
func SendResetPasswordMail(ctx context.Context, u *user_model.User) error {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
locale := translation.NewLocale(u.Language)
|
||||
sendUserMail(u.Language, u, mailAuthResetPassword, u.GenerateEmailActivateCode(u.Email), locale.TrString("mail.reset_password"), "recover account")
|
||||
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.PasswordReset)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return sendUserMail(u.Language, u, mailAuthResetPassword, code, locale.TrString("mail.reset_password"), "recover account")
|
||||
}
|
||||
|
||||
// SendActivateEmailMail sends confirmation email to confirm new email address
|
||||
func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
func SendActivateEmailMail(ctx context.Context, u *user_model.User, email string) error {
|
||||
if setting.MailService == nil {
|
||||
// No mail service configured
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
locale := translation.NewLocale(u.Language)
|
||||
code, err := u.GenerateEmailAuthorizationCode(ctx, auth_model.EmailActivation(email))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"locale": locale,
|
||||
"DisplayName": u.DisplayName(),
|
||||
"ActiveCodeLives": timeutil.MinutesToFriendly(setting.Service.ActiveCodeLives, locale),
|
||||
"Code": u.GenerateEmailActivateCode(email),
|
||||
"Code": code,
|
||||
"Email": email,
|
||||
"Language": locale.Language(),
|
||||
}
|
||||
|
@ -132,14 +151,14 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
|||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
return err
|
||||
}
|
||||
|
||||
msg := NewMessage(email, locale.TrString("mail.activate_email"), content.String())
|
||||
msg.Info = fmt.Sprintf("UID: %d, activate email", u.ID)
|
||||
|
||||
SendAsync(msg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SendRegisterNotifyMail triggers a notify e-mail by admin created a account.
|
||||
|
|
|
@ -22,9 +22,16 @@ import (
|
|||
//
|
||||
// The payload is verifiable by the generated HMAC using the user secret. It contains:
|
||||
// | Timestamp | Action/Handler Type | Action/Handler Data |
|
||||
//
|
||||
//
|
||||
// Version changelog
|
||||
//
|
||||
// v1 -> v2:
|
||||
// Use 128 instead of 80 bits of the HMAC-SHA256 output.
|
||||
|
||||
const (
|
||||
tokenVersion1 byte = 1
|
||||
tokenVersion2 byte = 2
|
||||
tokenLifetimeInYears int = 1
|
||||
)
|
||||
|
||||
|
@ -70,7 +77,7 @@ func CreateToken(ht HandlerType, user *user_model.User, data []byte) (string, er
|
|||
return "", err
|
||||
}
|
||||
|
||||
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion1}, packagedData...)), nil
|
||||
return encodingWithoutPadding.EncodeToString(append([]byte{tokenVersion2}, packagedData...)), nil
|
||||
}
|
||||
|
||||
// ExtractToken extracts the action/user tuple from the token and verifies the content
|
||||
|
@ -84,7 +91,7 @@ func ExtractToken(ctx context.Context, token string) (HandlerType, *user_model.U
|
|||
return UnknownHandlerType, nil, nil, &ErrToken{"no data"}
|
||||
}
|
||||
|
||||
if data[0] != tokenVersion1 {
|
||||
if data[0] != tokenVersion2 {
|
||||
return UnknownHandlerType, nil, nil, &ErrToken{fmt.Sprintf("unsupported token version: %v", data[0])}
|
||||
}
|
||||
|
||||
|
@ -124,5 +131,8 @@ func generateHmac(secret, payload []byte) []byte {
|
|||
mac.Write(payload)
|
||||
hmac := mac.Sum(nil)
|
||||
|
||||
return hmac[:10] // RFC2104 recommends not using less then 80 bits
|
||||
// RFC2104 section 5 recommends that if you do HMAC truncation, you should use
|
||||
// the max(80, hash_len/2) of the leftmost bits.
|
||||
// For SHA256 this works out to using 128 of the leftmost bits.
|
||||
return hmac[:16]
|
||||
}
|
||||
|
|
|
@ -307,9 +307,10 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
|
|||
}
|
||||
|
||||
releases, err := db.Find[repo_model.Release](ctx, repo_model.FindReleasesOptions{
|
||||
RepoID: repo.ID,
|
||||
TagNames: tags,
|
||||
IncludeTags: true,
|
||||
RepoID: repo.ID,
|
||||
TagNames: tags,
|
||||
IncludeDrafts: true,
|
||||
IncludeTags: true,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("db.Find[repo_model.Release]: %w", err)
|
||||
|
@ -394,13 +395,17 @@ func pushUpdateAddTags(ctx context.Context, repo *repo_model.Repository, gitRepo
|
|||
}
|
||||
newReleases = append(newReleases, rel)
|
||||
} else {
|
||||
rel.Title = parts[0]
|
||||
rel.Note = note
|
||||
rel.Sha1 = commit.ID.String()
|
||||
rel.CreatedUnix = timeutil.TimeStamp(createdAt.Unix())
|
||||
rel.NumCommits = commitsCount
|
||||
if rel.IsTag && author != nil {
|
||||
rel.PublisherID = author.ID
|
||||
if rel.IsTag {
|
||||
rel.Title = parts[0]
|
||||
rel.Note = note
|
||||
if author != nil {
|
||||
rel.PublisherID = author.ID
|
||||
}
|
||||
} else {
|
||||
rel.IsDraft = false
|
||||
}
|
||||
if err = repo_model.UpdateRelease(ctx, rel); err != nil {
|
||||
return fmt.Errorf("Update: %w", err)
|
||||
|
|
|
@ -96,6 +96,7 @@ func deleteUser(ctx context.Context, u *user_model.User, purge bool) (err error)
|
|||
&user_model.BlockedUser{BlockID: u.ID},
|
||||
&user_model.BlockedUser{UserID: u.ID},
|
||||
&actions_model.ActionRunnerToken{OwnerID: u.ID},
|
||||
&auth_model.AuthorizationToken{UID: u.ID},
|
||||
); err != nil {
|
||||
return fmt.Errorf("deleteBeans: %w", err)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
.ui.secondary.menu .dropdown.item > .menu { margin-top: 0; }
|
||||
</style>
|
||||
</noscript>
|
||||
{{template "shared/user/mention_highlight" .}}
|
||||
{{template "base/head_opengraph" .}}
|
||||
{{template "base/head_style" .}}
|
||||
{{template "custom/header" .}}
|
||||
|
|
14
templates/shared/user/mention_highlight.tmpl
Normal file
14
templates/shared/user/mention_highlight.tmpl
Normal file
|
@ -0,0 +1,14 @@
|
|||
{{if .IsSigned}}
|
||||
<style>
|
||||
.comment,
|
||||
.commit-summary,
|
||||
.commit-body {
|
||||
.mention[href="{{AppSubUrl}}/{{.SignedUser.Name}}" i] {
|
||||
background-color: var(--color-primary-alpha-30);
|
||||
color: var(--color-primary-dark-2);
|
||||
border-radius: 5px;
|
||||
padding: 1px 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
|
@ -20,7 +20,6 @@ const workflow_trigger_notification_text = 'This workflow has a workflow_dispatc
|
|||
|
||||
test('workflow dispatch present', async ({browser}, workerInfo) => {
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
/** @type {import('@playwright/test').Page} */
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
|
||||
|
@ -40,7 +39,6 @@ test('workflow dispatch error: missing inputs', async ({browser}, workerInfo) =>
|
|||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
|
||||
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
/** @type {import('@playwright/test').Page} */
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
|
||||
|
@ -62,14 +60,13 @@ test('workflow dispatch success', async ({browser}, workerInfo) => {
|
|||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Flaky behaviour on mobile safari; see https://codeberg.org/forgejo/forgejo/pulls/3334#issuecomment-2033383');
|
||||
|
||||
const context = await load_logged_in_context(browser, workerInfo, 'user2');
|
||||
/** @type {import('@playwright/test').Page} */
|
||||
const page = await context.newPage();
|
||||
|
||||
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
|
||||
|
||||
await page.locator('#workflow_dispatch_dropdown>button').click();
|
||||
|
||||
await page.type('input[name="inputs[string2]"]', 'abc');
|
||||
await page.fill('input[name="inputs[string2]"]', 'abc');
|
||||
await page.locator('#workflow-dispatch-submit').click();
|
||||
|
||||
await expect(page.getByText('Workflow run was successfully requested.')).toBeVisible();
|
||||
|
|
|
@ -5,7 +5,6 @@ package e2e
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -23,14 +22,31 @@ import (
|
|||
|
||||
// first entry represents filename
|
||||
// the following entries define the full file content over time
|
||||
type FileChanges [][]string
|
||||
type FileChanges struct {
|
||||
Filename string
|
||||
CommitMsg string
|
||||
Versions []string
|
||||
}
|
||||
|
||||
// put your Git repo declarations in here
|
||||
// feel free to amend the helper function below or use the raw variant directly
|
||||
func DeclareGitRepos(t *testing.T) func() {
|
||||
cleanupFunctions := []func(){
|
||||
newRepo(t, 2, "diff-test", FileChanges{
|
||||
{"testfile", "hello", "hallo", "hola", "native", "ubuntu-latest", "- runs-on: ubuntu-latest", "- runs-on: debian-latest"},
|
||||
newRepo(t, 2, "diff-test", []FileChanges{{
|
||||
Filename: "testfile",
|
||||
Versions: []string{"hello", "hallo", "hola", "native", "ubuntu-latest", "- runs-on: ubuntu-latest", "- runs-on: debian-latest"},
|
||||
}}),
|
||||
newRepo(t, 2, "mentions-highlighted", []FileChanges{
|
||||
{
|
||||
Filename: "history1.md",
|
||||
Versions: []string{""},
|
||||
CommitMsg: "A commit message which mentions @user2 in the title\nand has some additional text which mentions @user1",
|
||||
},
|
||||
{
|
||||
Filename: "history2.md",
|
||||
Versions: []string{""},
|
||||
CommitMsg: "Another commit which mentions @user1 in the title\nand @user2 in the text",
|
||||
},
|
||||
}),
|
||||
// add your repo declarations here
|
||||
}
|
||||
|
@ -42,7 +58,7 @@ func DeclareGitRepos(t *testing.T) func() {
|
|||
}
|
||||
}
|
||||
|
||||
func newRepo(t *testing.T, userID int64, repoName string, fileChanges FileChanges) func() {
|
||||
func newRepo(t *testing.T, userID int64, repoName string, fileChanges []FileChanges) func() {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: userID})
|
||||
somerepo, _, cleanupFunc := tests.CreateDeclarativeRepo(t, user, repoName,
|
||||
[]unit_model.Type{unit_model.TypeCode, unit_model.TypeIssues}, nil,
|
||||
|
@ -50,19 +66,25 @@ func newRepo(t *testing.T, userID int64, repoName string, fileChanges FileChange
|
|||
)
|
||||
|
||||
for _, file := range fileChanges {
|
||||
changeLen := len(file)
|
||||
for i := 1; i < changeLen; i++ {
|
||||
operation := "create"
|
||||
if i != 1 {
|
||||
operation = "update"
|
||||
for i, version := range file.Versions {
|
||||
operation := "update"
|
||||
if i == 0 {
|
||||
operation = "create"
|
||||
}
|
||||
|
||||
// default to unique commit messages
|
||||
commitMsg := file.CommitMsg
|
||||
if commitMsg == "" {
|
||||
commitMsg = fmt.Sprintf("Patch: %s-%d", file.Filename, i+1)
|
||||
}
|
||||
|
||||
resp, err := files_service.ChangeRepoFiles(git.DefaultContext, somerepo, user, &files_service.ChangeRepoFilesOptions{
|
||||
Files: []*files_service.ChangeRepoFile{{
|
||||
Operation: operation,
|
||||
TreePath: file[0],
|
||||
ContentReader: strings.NewReader(file[i]),
|
||||
TreePath: file.Filename,
|
||||
ContentReader: strings.NewReader(version),
|
||||
}},
|
||||
Message: fmt.Sprintf("Patch: %s-%s", file[0], strconv.Itoa(i)),
|
||||
Message: commitMsg,
|
||||
OldBranch: "main",
|
||||
NewBranch: "main",
|
||||
Author: &files_service.IdentityOptions{
|
||||
|
|
|
@ -21,10 +21,10 @@ test('Load Homepage', async ({page}) => {
|
|||
test('Register Form', async ({page}, workerInfo) => {
|
||||
const response = await page.goto('/user/sign_up');
|
||||
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');
|
||||
await page.fill('input[name=user_name]', `e2e-test-${workerInfo.workerIndex}`);
|
||||
await page.fill('input[name=email]', `e2e-test-${workerInfo.workerIndex}@test.com`);
|
||||
await page.fill('input[name=password]', 'test123test123');
|
||||
await page.fill('input[name=retype]', 'test123test123');
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
// Make sure we routed to the home page. Else login failed.
|
||||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
|
|
|
@ -4,19 +4,18 @@
|
|||
// web_src/js/features/repo-issue**
|
||||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["check_wip"] }] */
|
||||
|
||||
import {expect, type Page} from '@playwright/test';
|
||||
import {test, login_user, login} from './utils_e2e.ts';
|
||||
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
/* eslint-disable playwright/expect-expect */
|
||||
// some tests are reported to have no assertions,
|
||||
// which is not correct, because they use the global helper function
|
||||
test.describe('Pull: Toggle WIP', () => {
|
||||
const prTitle = 'pull5';
|
||||
async function toggle_wip_to({page}, should) {
|
||||
async function toggle_wip_to({page}, should: boolean) {
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
if (should) {
|
||||
await page.getByText('Still in progress?').click();
|
||||
|
@ -25,7 +24,7 @@ test.describe('Pull: Toggle WIP', () => {
|
|||
}
|
||||
}
|
||||
|
||||
async function check_wip({page}, is) {
|
||||
async function check_wip({page}, is: boolean) {
|
||||
const elemTitle = 'h1';
|
||||
const stateLabel = '.issue-state-label';
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
@ -96,12 +95,11 @@ test.describe('Pull: Toggle WIP', () => {
|
|||
await expect(page.locator('h1')).toContainText(maxLenStr);
|
||||
});
|
||||
});
|
||||
/* eslint-enable playwright/expect-expect */
|
||||
|
||||
test('Issue: Labels', async ({browser}, workerInfo) => {
|
||||
test.skip(workerInfo.project.name === 'Mobile Safari', 'Unable to get tests working on Safari Mobile, see https://codeberg.org/forgejo/forgejo/pulls/3445#issuecomment-1789636');
|
||||
|
||||
async function submitLabels({page}) {
|
||||
async function submitLabels({page}: {page: Page}) {
|
||||
const submitted = page.waitForResponse('/user2/repo1/issues/labels');
|
||||
await page.locator('textarea').first().click(); // close via unrelated element
|
||||
await submitted;
|
||||
|
@ -199,7 +197,7 @@ test('New Issue: Assignees', async ({browser}, workerInfo) => {
|
|||
|
||||
// Assign other user (with searchbox)
|
||||
await page.locator('.select-assignees.dropdown').click();
|
||||
await page.type('.select-assignees .menu .search input', 'user4');
|
||||
await page.fill('.select-assignees .menu .search input', 'user4');
|
||||
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user2'})).toBeHidden();
|
||||
await expect(page.locator('.select-assignees .menu .item').filter({hasText: 'user4'})).toBeVisible();
|
||||
await page.locator('.select-assignees .menu .item').filter({hasText: 'user4'}).click();
|
||||
|
|
|
@ -29,7 +29,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
|
|||
|
||||
// Indent, then unindent first line
|
||||
await textarea.focus();
|
||||
await textarea.evaluate((it) => it.setSelectionRange(0, 0));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(0, 0));
|
||||
await indent.click();
|
||||
await expect(textarea).toHaveValue(`${tab}* first\n* second\n* third\n* last`);
|
||||
await unindent.click();
|
||||
|
@ -45,7 +45,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
|
|||
|
||||
// Subsequently, select a chunk of 2nd and 3rd line and indent both, preserving the cursor position in relation to text
|
||||
await textarea.focus();
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird')));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('hird')));
|
||||
await indent.click();
|
||||
const lines23 = `* first\n${tab}${tab}* second\n${tab}* third\n* last`;
|
||||
await expect(textarea).toHaveValue(lines23);
|
||||
|
@ -60,7 +60,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
|
|||
|
||||
// Indent and unindent with cursor at the end of the line
|
||||
await textarea.focus();
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
|
||||
await textarea.press('End');
|
||||
await indent.click();
|
||||
await expect(textarea).toHaveValue(`* first\n${tab}* second\n* third\n* last`);
|
||||
|
@ -69,7 +69,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
|
|||
|
||||
// Check that Tab does work after input
|
||||
await textarea.focus();
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.press('Shift+Enter'); // Avoid triggering the prefix continuation feature
|
||||
await textarea.pressSequentially('* least');
|
||||
await indent.click();
|
||||
|
@ -78,7 +78,7 @@ test('markdown indentation', async ({browser}, workerInfo) => {
|
|||
// Check that partial indents are cleared
|
||||
await textarea.focus();
|
||||
await textarea.fill(initText);
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second')));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('* second'), it.value.indexOf('* second')));
|
||||
await textarea.pressSequentially(' ');
|
||||
await unindent.click();
|
||||
await expect(textarea).toHaveValue(initText);
|
||||
|
@ -99,7 +99,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
|
|||
await textarea.fill(initText);
|
||||
|
||||
// Test continuation of '* ' prefix
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.indexOf('cond'), it.value.indexOf('cond')));
|
||||
await textarea.press('End');
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('middle');
|
||||
|
@ -112,7 +112,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
|
|||
await expect(textarea).toHaveValue(`* first\n* second\n${tab}* middle\n${tab}* muddle\n* third\n* last`);
|
||||
|
||||
// Test breaking in the middle of a line
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle')));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.lastIndexOf('ddle'), it.value.lastIndexOf('ddle')));
|
||||
await textarea.pressSequentially('tate');
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('me');
|
||||
|
@ -120,7 +120,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
|
|||
|
||||
// Test not triggering when Shift held
|
||||
await textarea.fill(initText);
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.press('Shift+Enter');
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('...but not least');
|
||||
|
@ -128,28 +128,28 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
|
|||
|
||||
// Test continuation of ordered list
|
||||
await textarea.fill(`1. one\n2. two`);
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('three');
|
||||
await expect(textarea).toHaveValue(`1. one\n2. two\n3. three`);
|
||||
|
||||
// Test continuation of alternative ordered list syntax
|
||||
await textarea.fill(`1) one\n2) two`);
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('three');
|
||||
await expect(textarea).toHaveValue(`1) one\n2) two\n3) three`);
|
||||
|
||||
// Test continuation of blockquote
|
||||
await textarea.fill(`> knowledge is power`);
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('france is bacon');
|
||||
await expect(textarea).toHaveValue(`> knowledge is power\n> france is bacon`);
|
||||
|
||||
// Test continuation of checklists
|
||||
await textarea.fill(`- [ ] have a problem\n- [x] create a solution`);
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('write a test');
|
||||
await expect(textarea).toHaveValue(`- [ ] have a problem\n- [x] create a solution\n- [ ] write a test`);
|
||||
|
@ -174,7 +174,7 @@ test('markdown list continuation', async ({browser}, workerInfo) => {
|
|||
];
|
||||
for (const prefix of prefixes) {
|
||||
await textarea.fill(`${prefix}one`);
|
||||
await textarea.evaluate((it) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.evaluate((it:HTMLTextAreaElement) => it.setSelectionRange(it.value.length, it.value.length));
|
||||
await textarea.press('Enter');
|
||||
await textarea.pressSequentially('two');
|
||||
await expect(textarea).toHaveValue(`${prefix}one\n${prefix}two`);
|
||||
|
|
|
@ -3,14 +3,14 @@
|
|||
// routers/web/repo/issue.go
|
||||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {expect, type Locator} from '@playwright/test';
|
||||
import {test, login_user, load_logged_in_context} from './utils_e2e.ts';
|
||||
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
const assertReactionCounts = (comment, counts) =>
|
||||
const assertReactionCounts = (comment: Locator, counts: unknown) =>
|
||||
expect(async () => {
|
||||
await expect(comment.locator('.reactions')).toBeVisible();
|
||||
|
||||
|
@ -29,7 +29,7 @@ const assertReactionCounts = (comment, counts) =>
|
|||
return expect(reactions).toStrictEqual(counts);
|
||||
}).toPass();
|
||||
|
||||
async function toggleReaction(menu, reaction) {
|
||||
async function toggleReaction(menu: Locator, reaction: string) {
|
||||
await menu.evaluateAll((menus) => menus[0].focus());
|
||||
await menu.locator('.add-reaction').click();
|
||||
await menu.locator(`[role=menuitem][data-reaction-content="${reaction}"]`).click();
|
||||
|
|
|
@ -4,10 +4,15 @@
|
|||
// services/gitdiff/**
|
||||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {expect, type Page} from '@playwright/test';
|
||||
import {test, login_user, login} from './utils_e2e.ts';
|
||||
import {accessibilityCheck} from './shared/accessibility.ts';
|
||||
|
||||
async function assertSelectedLines(page, nums) {
|
||||
test.beforeAll(async ({browser}, workerInfo) => {
|
||||
await login_user(browser, workerInfo, 'user2');
|
||||
});
|
||||
|
||||
async function assertSelectedLines(page: Page, nums: string[]) {
|
||||
const pageAssertions = async () => {
|
||||
expect(
|
||||
await Promise.all((await page.locator('tr.active [data-line-number]').all()).map((line) => line.getAttribute('data-line-number'))),
|
||||
|
@ -75,3 +80,19 @@ test('Readable diff', async ({page}, workerInfo) => {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('Username highlighted in commits', async ({browser}, workerInfo) => {
|
||||
const page = await login({browser}, workerInfo);
|
||||
await page.goto('/user2/mentions-highlighted/commits/branch/main');
|
||||
// check first commit
|
||||
await page.getByRole('link', {name: 'A commit message which'}).click();
|
||||
await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/);
|
||||
await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await accessibilityCheck({page}, ['.commit-header'], [], []);
|
||||
// check second commit
|
||||
await page.goto('/user2/mentions-highlighted/commits/branch/main');
|
||||
await page.locator('tbody').getByRole('link', {name: 'Another commit which mentions'}).click();
|
||||
await expect(page.getByRole('link', {name: '@user2'})).toHaveCSS('background-color', /(.*)/);
|
||||
await expect(page.getByRole('link', {name: '@user1'})).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
||||
await accessibilityCheck({page}, ['.commit-header'], [], []);
|
||||
});
|
||||
|
|
35
tests/e2e/shared/accessibility.ts
Normal file
35
tests/e2e/shared/accessibility.ts
Normal file
|
@ -0,0 +1,35 @@
|
|||
import {expect, type Page} from '@playwright/test';
|
||||
import {AxeBuilder} from '@axe-core/playwright';
|
||||
|
||||
export async function accessibilityCheck({page}: {page: Page}, includes: string[], excludes: string[], disabledRules: string[]) {
|
||||
// contrast of inline links is still a global issue in Forgejo
|
||||
disabledRules.push('link-in-text-block');
|
||||
|
||||
let accessibilityScanner = new AxeBuilder({page})
|
||||
.disableRules(disabledRules);
|
||||
// passing the whole array seems to be not supported,
|
||||
// iterating has the nice side-effectof skipping this if the array is empty
|
||||
for (const incl of includes) {
|
||||
// passing the whole array seems to be not supported
|
||||
accessibilityScanner = accessibilityScanner.include(incl);
|
||||
}
|
||||
for (const excl of excludes) {
|
||||
accessibilityScanner = accessibilityScanner.exclude(excl);
|
||||
}
|
||||
|
||||
// scan the page both in dark and light theme
|
||||
let accessibilityScanResults = await accessibilityScanner.analyze();
|
||||
expect(accessibilityScanResults.violations).toEqual([]);
|
||||
await page.emulateMedia({colorScheme: 'dark'});
|
||||
// in https://codeberg.org/forgejo/forgejo/pulls/5899 there have been
|
||||
// some weird failures related to contrast scanning,
|
||||
// reporting for colours that haven't been used and no trace in the
|
||||
// screenshots.
|
||||
// Since this was only happening with some browsers and not always,
|
||||
// my bet is on a transition effect on dark/light mode switch.
|
||||
// Waiting a little seems to work around this.
|
||||
await page.waitForTimeout(100); // eslint-disable-line playwright/no-wait-for-timeout
|
||||
accessibilityScanResults = await accessibilityScanner.analyze();
|
||||
expect(accessibilityScanResults.violations).toEqual([]);
|
||||
await page.emulateMedia({colorScheme: 'light'});
|
||||
}
|
|
@ -1,17 +1,14 @@
|
|||
import {expect, type Page} from '@playwright/test';
|
||||
import {AxeBuilder} from '@axe-core/playwright';
|
||||
import {accessibilityCheck} from './accessibility.ts';
|
||||
|
||||
export async function validate_form({page}: {page: Page}, scope: 'form' | 'fieldset' = 'form') {
|
||||
const accessibilityScanResults = await new AxeBuilder({page})
|
||||
// disable checking for link style - should be fixed, but not now
|
||||
.disableRules('link-in-text-block')
|
||||
.include(scope)
|
||||
const excludedElements = [
|
||||
// exclude automated tooltips from accessibility scan, remove when fixed
|
||||
.exclude('span[data-tooltip-content')
|
||||
'span[data-tooltip-content',
|
||||
// exclude weird non-semantic HTML disabled content
|
||||
.exclude('.disabled')
|
||||
.analyze();
|
||||
expect(accessibilityScanResults.violations).toEqual([]);
|
||||
'.disabled',
|
||||
];
|
||||
await accessibilityCheck({page}, [scope], excludedElements, []);
|
||||
|
||||
// assert CSS properties that needed to be overriden for forms (ensure they remain active)
|
||||
const boxes = page.getByRole('checkbox').or(page.getByRole('radio'));
|
||||
|
|
|
@ -33,8 +33,8 @@ export async function login_user(browser: Browser, workerInfo: TestInfo, user: s
|
|||
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.fill('input[name=user_name]', user);
|
||||
await page.fill('input[name=password]', LOGIN_PASSWORD);
|
||||
await page.click('form button.ui.primary.button:visible');
|
||||
|
||||
await page.waitForLoadState();
|
||||
|
@ -48,15 +48,13 @@ export async function login_user(browser: Browser, workerInfo: TestInfo, user: s
|
|||
}
|
||||
|
||||
export async function load_logged_in_context(browser: Browser, workerInfo: TestInfo, user: string) {
|
||||
let context;
|
||||
try {
|
||||
context = await test_context(browser, {storageState: `${ARTIFACTS_PATH}/state-${user}-${workerInfo.workerIndex}.json`});
|
||||
return await test_context(browser, {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 login({browser}: {browser: Browser}, workerInfo: TestInfo) {
|
||||
|
|
|
@ -8,6 +8,8 @@ import (
|
|||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
@ -17,6 +19,8 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var rootPathRe = regexp.MustCompile("\\[repository\\]\nROOT\\s=\\s.*")
|
||||
|
||||
func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare ...bool) {
|
||||
if len(prepare) == 0 || prepare[0] {
|
||||
defer tests.PrepareTestEnv(t, 1)()
|
||||
|
@ -37,7 +41,13 @@ func onForgejoRunTB(t testing.TB, callback func(testing.TB, *url.URL), prepare .
|
|||
require.NoError(t, err)
|
||||
u.Host = listener.Addr().String()
|
||||
|
||||
// Override repository root in config.
|
||||
conf, err := os.ReadFile(setting.CustomConf)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(setting.CustomConf, rootPathRe.ReplaceAll(conf, []byte("[repository]\nROOT = "+setting.RepoRootPath)), 0o644))
|
||||
|
||||
defer func() {
|
||||
require.NoError(t, os.WriteFile(setting.CustomConf, conf, 0o644))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
s.Shutdown(ctx)
|
||||
cancel()
|
||||
|
|
|
@ -30,7 +30,6 @@ test('WebAuthn register & login flow', async ({browser, request}, workerInfo) =>
|
|||
transport: 'usb',
|
||||
automaticPresenceSimulation: true,
|
||||
isUserVerified: true,
|
||||
backupEligibility: true, // TODO: this doesn't seem to be available?!
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -109,4 +109,24 @@ func TestFeed(t *testing.T) {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("View permission", func(t *testing.T) {
|
||||
t.Run("Anomynous", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master")
|
||||
MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
t.Run("No code permission", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
session := loginUser(t, "user8")
|
||||
req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master")
|
||||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
t.Run("With code permission", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
session := loginUser(t, "user9")
|
||||
req := NewRequest(t, "GET", "/org3/repo3/rss/branch/master")
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ import (
|
|||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/routers"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestAPIForkAsAdminIgnoringLimits(t *testing.T) {
|
||||
|
@ -106,3 +108,44 @@ func TestAPIDisabledForkRepo(t *testing.T) {
|
|||
session.MakeRequest(t, req, http.StatusNotFound)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIForkListPrivateRepo(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
session := loginUser(t, "user5")
|
||||
token := getTokenForLoggedInUser(t, session,
|
||||
auth_model.AccessTokenScopeWriteRepository,
|
||||
auth_model.AccessTokenScopeWriteOrganization)
|
||||
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23, Visibility: api.VisibleTypePrivate})
|
||||
|
||||
req := NewRequestWithJSON(t, "POST", "/api/v1/repos/user2/repo1/forks", &api.CreateForkOption{
|
||||
Organization: &org23.Name,
|
||||
}).AddTokenAuth(token)
|
||||
MakeRequest(t, req, http.StatusAccepted)
|
||||
|
||||
t.Run("Anomynous", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var forks []*api.Repository
|
||||
DecodeJSON(t, resp, &forks)
|
||||
|
||||
assert.Empty(t, forks)
|
||||
assert.EqualValues(t, "0", resp.Header().Get("X-Total-Count"))
|
||||
})
|
||||
|
||||
t.Run("Logged in", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/repos/user2/repo1/forks").AddTokenAuth(token)
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
var forks []*api.Repository
|
||||
DecodeJSON(t, resp, &forks)
|
||||
|
||||
assert.Len(t, forks, 1)
|
||||
assert.EqualValues(t, "1", resp.Header().Get("X-Total-Count"))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/pquerna/otp/totp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -58,3 +59,24 @@ func TestAPITwoFactor(t *testing.T) {
|
|||
req.Header.Set("X-Forgejo-OTP", passcode)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestAPIWebAuthn(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 32})
|
||||
unittest.AssertExistsAndLoadBean(t, &auth_model.WebAuthnCredential{UserID: user.ID})
|
||||
|
||||
req := NewRequest(t, "GET", "/api/v1/user")
|
||||
req.SetBasicAuth(user.Name, "notpassword")
|
||||
|
||||
resp := MakeRequest(t, req, http.StatusUnauthorized)
|
||||
|
||||
type userResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
var userParsed userResponse
|
||||
|
||||
DecodeJSON(t, resp, &userParsed)
|
||||
|
||||
assert.EqualValues(t, "Basic authorization is not allowed while having security keys enrolled", userParsed.Message)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
|
@ -206,11 +207,11 @@ func TestAPIListWikiPages(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAPINewWikiPage(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
for _, title := range []string{
|
||||
"New page",
|
||||
"&&&&",
|
||||
} {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
username := "user2"
|
||||
session := loginUser(t, username)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
@ -386,26 +387,26 @@ func TestAPIListPageRevisions(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestAPIWikiNonMasterBranch(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{
|
||||
WikiBranch: optional.Some("main"),
|
||||
})
|
||||
defer f()
|
||||
|
||||
uris := []string{
|
||||
"revisions/Home",
|
||||
"pages",
|
||||
"page/Home",
|
||||
}
|
||||
baseURL := fmt.Sprintf("/api/v1/repos/%s/wiki", repo.FullName())
|
||||
for _, uri := range uris {
|
||||
t.Run(uri, func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestf(t, "GET", "%s/%s", baseURL, uri)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
onGiteaRun(t, func(t *testing.T, _ *url.URL) {
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 1})
|
||||
repo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{
|
||||
WikiBranch: optional.Some("main"),
|
||||
})
|
||||
}
|
||||
defer f()
|
||||
|
||||
uris := []string{
|
||||
"revisions/Home",
|
||||
"pages",
|
||||
"page/Home",
|
||||
}
|
||||
baseURL := fmt.Sprintf("/api/v1/repos/%s/wiki", repo.FullName())
|
||||
for _, uri := range uris {
|
||||
t.Run(uri, func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestf(t, "GET", "%s/%s", baseURL, uri)
|
||||
MakeRequest(t, req, http.StatusOK)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -84,7 +84,7 @@ func TestLTACookie(t *testing.T) {
|
|||
assert.True(t, found)
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{LookupKey: lookupKey, HashedValidator: auth.HashValidator(rawValidator), UID: user.ID, Purpose: auth.LongTermAuthorization})
|
||||
|
||||
// Check if the LTA cookie it provides authentication.
|
||||
// If LTA cookie provides authentication /user/login shouldn't return status 200.
|
||||
|
@ -143,7 +143,7 @@ func TestLTAExpiry(t *testing.T) {
|
|||
assert.True(t, found)
|
||||
|
||||
// Ensure it's not expired.
|
||||
lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
|
||||
lta := unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
|
||||
assert.False(t, lta.IsExpired())
|
||||
|
||||
// Manually stub LTA's expiry.
|
||||
|
@ -151,7 +151,7 @@ func TestLTAExpiry(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
|
||||
// Ensure it's expired.
|
||||
lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
|
||||
lta = unittest.AssertExistsAndLoadBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
|
||||
assert.True(t, lta.IsExpired())
|
||||
|
||||
// Should return 200 OK, because LTA doesn't provide authorization anymore.
|
||||
|
@ -160,5 +160,5 @@ func TestLTAExpiry(t *testing.T) {
|
|||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
// Ensure it's deleted.
|
||||
unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey})
|
||||
unittest.AssertNotExistsBean(t, &auth.AuthorizationToken{UID: user.ID, LookupKey: lookupKey, Purpose: auth.LongTermAuthorization})
|
||||
}
|
||||
|
|
|
@ -6,9 +6,12 @@ package integration
|
|||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/tests"
|
||||
)
|
||||
|
||||
func TestEasyMDESwitch(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
session := loginUser(t, "user2")
|
||||
testEasyMDESwitch(t, session, "user2/glob/issues/1", false)
|
||||
testEasyMDESwitch(t, session, "user2/glob/issues/new", false)
|
||||
|
|
21
tests/integration/fixtures/TestFeed/team.yml
Normal file
21
tests/integration/fixtures/TestFeed/team.yml
Normal file
|
@ -0,0 +1,21 @@
|
|||
-
|
||||
id: 1001
|
||||
org_id: 3
|
||||
lower_name: no_code
|
||||
name: no_code
|
||||
authorize: 1 # read
|
||||
num_repos: 1
|
||||
num_members: 1
|
||||
includes_all_repositories: false
|
||||
can_create_org_repo: false
|
||||
|
||||
-
|
||||
id: 1002
|
||||
org_id: 3
|
||||
lower_name: read_code
|
||||
name: no_code
|
||||
authorize: 1 # read
|
||||
num_repos: 1
|
||||
num_members: 1
|
||||
includes_all_repositories: false
|
||||
can_create_org_repo: false
|
11
tests/integration/fixtures/TestFeed/team_repo.yml
Normal file
11
tests/integration/fixtures/TestFeed/team_repo.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
-
|
||||
id: 1001
|
||||
org_id: 3
|
||||
team_id: 1001
|
||||
repo_id: 3
|
||||
|
||||
-
|
||||
id: 1002
|
||||
org_id: 3
|
||||
team_id: 1002
|
||||
repo_id: 3
|
83
tests/integration/fixtures/TestFeed/team_unit.yml
Normal file
83
tests/integration/fixtures/TestFeed/team_unit.yml
Normal file
|
@ -0,0 +1,83 @@
|
|||
-
|
||||
id: 1001
|
||||
team_id: 1001
|
||||
type: 1
|
||||
access_mode: 0
|
||||
|
||||
-
|
||||
id: 1002
|
||||
team_id: 1001
|
||||
type: 2
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1003
|
||||
team_id: 1001
|
||||
type: 3
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1004
|
||||
team_id: 1001
|
||||
type: 4
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1005
|
||||
team_id: 1001
|
||||
type: 5
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1006
|
||||
team_id: 1001
|
||||
type: 6
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1007
|
||||
team_id: 1001
|
||||
type: 7
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1008
|
||||
team_id: 1002
|
||||
type: 1
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1009
|
||||
team_id: 1002
|
||||
type: 2
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1010
|
||||
team_id: 1002
|
||||
type: 3
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1011
|
||||
team_id: 1002
|
||||
type: 4
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1012
|
||||
team_id: 1002
|
||||
type: 5
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1013
|
||||
team_id: 1002
|
||||
type: 6
|
||||
access_mode: 1
|
||||
|
||||
-
|
||||
id: 1014
|
||||
team_id: 1002
|
||||
type: 7
|
||||
access_mode: 1
|
11
tests/integration/fixtures/TestFeed/team_user.yml
Normal file
11
tests/integration/fixtures/TestFeed/team_user.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
-
|
||||
id: 1001
|
||||
org_id: 3
|
||||
team_id: 1001
|
||||
uid: 8
|
||||
|
||||
-
|
||||
id: 1002
|
||||
org_id: 3
|
||||
team_id: 1002
|
||||
uid: 9
|
|
@ -13,7 +13,6 @@ import (
|
|||
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -33,8 +32,6 @@ func assertFileEqual(t *testing.T, p string, content []byte) {
|
|||
|
||||
func TestRepoCloneWiki(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
dstPath := t.TempDir()
|
||||
|
||||
r := fmt.Sprintf("%suser2/repo1.wiki.git", u.String())
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
@ -59,6 +60,8 @@ func createSSHUrl(gitPath string, u *url.URL) *url.URL {
|
|||
return &u2
|
||||
}
|
||||
|
||||
var rootPathRe = regexp.MustCompile("\\[repository\\]\nROOT\\s=\\s.*")
|
||||
|
||||
func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) {
|
||||
defer tests.PrepareTestEnv(t, 1)()
|
||||
s := http.Server{
|
||||
|
@ -77,7 +80,13 @@ func onGiteaRun[T testing.TB](t T, callback func(T, *url.URL)) {
|
|||
require.NoError(t, err)
|
||||
u.Host = listener.Addr().String()
|
||||
|
||||
// Override repository root in config.
|
||||
conf, err := os.ReadFile(setting.CustomConf)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, os.WriteFile(setting.CustomConf, rootPathRe.ReplaceAll(conf, []byte("[repository]\nROOT = "+setting.RepoRootPath)), 0o600))
|
||||
|
||||
defer func() {
|
||||
require.NoError(t, os.WriteFile(setting.CustomConf, conf, 0o600))
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
s.Shutdown(ctx)
|
||||
cancel()
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"encoding/base32"
|
||||
"io"
|
||||
"net"
|
||||
"net/smtp"
|
||||
|
@ -75,6 +76,51 @@ func TestIncomingEmail(t *testing.T) {
|
|||
assert.Equal(t, payload, p)
|
||||
})
|
||||
|
||||
tokenEncoding := base32.StdEncoding.WithPadding(base32.NoPadding)
|
||||
t.Run("Deprecated token version", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
payload := []byte{1, 2, 3, 4, 5}
|
||||
|
||||
token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
// Set the token to version 1.
|
||||
unencodedToken, err := tokenEncoding.DecodeString(token)
|
||||
require.NoError(t, err)
|
||||
unencodedToken[0] = 1
|
||||
token = tokenEncoding.EncodeToString(unencodedToken)
|
||||
|
||||
ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token)
|
||||
require.ErrorContains(t, err, "unsupported token version: 1")
|
||||
assert.Equal(t, token_service.UnknownHandlerType, ht)
|
||||
assert.Nil(t, u)
|
||||
assert.Nil(t, p)
|
||||
})
|
||||
|
||||
t.Run("MAC check", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
payload := []byte{1, 2, 3, 4, 5}
|
||||
|
||||
token, err := token_service.CreateToken(token_service.ReplyHandlerType, user, payload)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, token)
|
||||
|
||||
// Modify the MAC.
|
||||
unencodedToken, err := tokenEncoding.DecodeString(token)
|
||||
require.NoError(t, err)
|
||||
unencodedToken[len(unencodedToken)-1] ^= 0x01
|
||||
token = tokenEncoding.EncodeToString(unencodedToken)
|
||||
|
||||
ht, u, p, err := token_service.ExtractToken(db.DefaultContext, token)
|
||||
require.ErrorContains(t, err, "verification failed")
|
||||
assert.Equal(t, token_service.UnknownHandlerType, ht)
|
||||
assert.Nil(t, u)
|
||||
assert.Nil(t, p)
|
||||
})
|
||||
|
||||
t.Run("Handler", func(t *testing.T) {
|
||||
t.Run("Reply", func(t *testing.T) {
|
||||
checkReply := func(t *testing.T, payload []byte, issue *issues_model.Issue, commentType issues_model.CommentType) {
|
||||
|
|
21
tests/integration/mention_test.go
Normal file
21
tests/integration/mention_test.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
// Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestHeadMentionCSS(t *testing.T) {
|
||||
userSession := loginUser(t, "user2")
|
||||
resp := userSession.MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK)
|
||||
assert.Contains(t, resp.Body.String(), `.mention[href="/user2" i]`)
|
||||
|
||||
guestSession := emptyTestSession(t)
|
||||
resp = guestSession.MakeRequest(t, NewRequest(t, "GET", "/"), http.StatusOK)
|
||||
assert.NotContains(t, resp.Body.String(), `.mention[href="`)
|
||||
}
|
|
@ -323,3 +323,82 @@ func TestSSHPushMirror(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestPushMirrorSettings(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
defer test.MockVariableValue(&setting.Migrations.AllowLocalNetworks, true)()
|
||||
defer test.MockVariableValue(&setting.Mirror.Enabled, true)()
|
||||
require.NoError(t, migrations.Init())
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
srcRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 2})
|
||||
srcRepo2 := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 3})
|
||||
assert.False(t, srcRepo.HasWiki())
|
||||
sess := loginUser(t, user.Name)
|
||||
pushToRepo, _, f := tests.CreateDeclarativeRepoWithOptions(t, user, tests.DeclarativeRepoOptions{
|
||||
Name: optional.Some("push-mirror-test"),
|
||||
AutoInit: optional.Some(false),
|
||||
EnabledUnits: optional.Some([]unit.Type{unit.TypeCode}),
|
||||
})
|
||||
defer f()
|
||||
|
||||
t.Run("Adding", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo2.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo2.FullName())),
|
||||
"action": "push-mirror-add",
|
||||
"push_mirror_address": u.String() + pushToRepo.FullName(),
|
||||
"push_mirror_interval": "0",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())),
|
||||
"action": "push-mirror-add",
|
||||
"push_mirror_address": u.String() + pushToRepo.FullName(),
|
||||
"push_mirror_interval": "0",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
flashCookie := sess.GetCookie(gitea_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Contains(t, flashCookie.Value, "success")
|
||||
})
|
||||
|
||||
mirrors, _, err := repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo.ID, db.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, mirrors, 1)
|
||||
mirrorID := mirrors[0].ID
|
||||
|
||||
mirrors, _, err = repo_model.GetPushMirrorsByRepoID(db.DefaultContext, srcRepo2.ID, db.ListOptions{})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, mirrors, 1)
|
||||
|
||||
t.Run("Interval", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
unittest.AssertExistsAndLoadBean(t, &repo_model.PushMirror{ID: mirrorID - 1})
|
||||
|
||||
req := NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())),
|
||||
"action": "push-mirror-update",
|
||||
"push_mirror_id": strconv.FormatInt(mirrorID-1, 10),
|
||||
"push_mirror_interval": "10m0s",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusNotFound)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", fmt.Sprintf("/%s/settings", srcRepo.FullName()), map[string]string{
|
||||
"_csrf": GetCSRF(t, sess, fmt.Sprintf("/%s/settings", srcRepo.FullName())),
|
||||
"action": "push-mirror-update",
|
||||
"push_mirror_id": strconv.FormatInt(mirrorID, 10),
|
||||
"push_mirror_interval": "10m0s",
|
||||
})
|
||||
sess.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
flashCookie := sess.GetCookie(gitea_context.CookieNameFlash)
|
||||
assert.NotNil(t, flashCookie)
|
||||
assert.Contains(t, flashCookie.Value, "success")
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
"code.gitea.io/gitea/models/organization"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
|
@ -293,8 +294,10 @@ func TestOrgTeamEmailInviteRedirectsNewUserWithActivation(t *testing.T) {
|
|||
require.NoError(t, err)
|
||||
session.jar.SetCookies(baseURL, cr.Cookies())
|
||||
|
||||
activateURL := fmt.Sprintf("/user/activate?code=%s", user.GenerateEmailActivateCode("doesnotexist@example.com"))
|
||||
req = NewRequestWithValues(t, "POST", activateURL, map[string]string{
|
||||
code, err := user.GenerateEmailAuthorizationCode(db.DefaultContext, auth.UserActivation)
|
||||
require.NoError(t, err)
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/activate?code="+url.QueryEscape(code), map[string]string{
|
||||
"password": "examplePassword!1",
|
||||
})
|
||||
|
||||
|
|
|
@ -17,6 +17,7 @@ import (
|
|||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/routers"
|
||||
repo_service "code.gitea.io/gitea/services/repository"
|
||||
|
@ -238,3 +239,34 @@ func TestRepoForkToOrg(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestForkListPrivateRepo(t *testing.T) {
|
||||
forkItemSelector := ".tw-flex.tw-items-center.tw-py-2"
|
||||
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
session := loginUser(t, "user5")
|
||||
org23 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 23, Visibility: structs.VisibleTypePrivate})
|
||||
|
||||
testRepoFork(t, session, "user2", "repo1", org23.Name, "repo1")
|
||||
|
||||
t.Run("Anomynous", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo1/forks")
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
htmlDoc.AssertElement(t, forkItemSelector, false)
|
||||
})
|
||||
|
||||
t.Run("Logged in", func(t *testing.T) {
|
||||
defer tests.PrintCurrentTest(t)()
|
||||
|
||||
req := NewRequest(t, "GET", "/user2/repo1/forks")
|
||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||
htmlDoc := NewHTMLParser(t, resp.Body)
|
||||
|
||||
htmlDoc.AssertElement(t, forkItemSelector, true)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,17 +5,20 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/models"
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
"code.gitea.io/gitea/models/unittest"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
repo_module "code.gitea.io/gitea/modules/repository"
|
||||
api "code.gitea.io/gitea/modules/structs"
|
||||
"code.gitea.io/gitea/services/release"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
|
@ -159,3 +162,47 @@ func TestSyncRepoTags(t *testing.T) {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestRepushTag(t *testing.T) {
|
||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
session := loginUser(t, owner.LowerName)
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
|
||||
httpContext := NewAPITestContext(t, owner.Name, repo.Name)
|
||||
|
||||
dstPath := t.TempDir()
|
||||
|
||||
u.Path = httpContext.GitPath()
|
||||
u.User = url.UserPassword(owner.Name, userPassword)
|
||||
|
||||
doGitClone(dstPath, u)(t)
|
||||
|
||||
// create and push a tag
|
||||
_, _, err := git.NewCommand(git.DefaultContext, "tag", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
|
||||
require.NoError(t, err)
|
||||
_, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--tags", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
|
||||
require.NoError(t, err)
|
||||
// create a release for the tag
|
||||
createdRelease := createNewReleaseUsingAPI(t, token, owner, repo, "v2.0", "", "Release of v2.0", "desc")
|
||||
assert.False(t, createdRelease.IsDraft)
|
||||
// delete the tag
|
||||
_, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--delete", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
|
||||
require.NoError(t, err)
|
||||
// query the release by API and it should be a draft
|
||||
req := NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0"))
|
||||
resp := MakeRequest(t, req, http.StatusOK)
|
||||
var respRelease *api.Release
|
||||
DecodeJSON(t, resp, &respRelease)
|
||||
assert.True(t, respRelease.IsDraft)
|
||||
// re-push the tag
|
||||
_, _, err = git.NewCommand(git.DefaultContext, "push", "origin", "--tags", "v2.0").RunStdString(&git.RunOpts{Dir: dstPath})
|
||||
require.NoError(t, err)
|
||||
// query the release by API and it should not be a draft
|
||||
req = NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/releases/tags/%s", owner.Name, repo.Name, "v2.0"))
|
||||
resp = MakeRequest(t, req, http.StatusOK)
|
||||
DecodeJSON(t, resp, &respRelease)
|
||||
assert.False(t, respRelease.IsDraft)
|
||||
})
|
||||
}
|
||||
|
|
|
@ -5,14 +5,18 @@
|
|||
package integration
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
auth_model "code.gitea.io/gitea/models/auth"
|
||||
"code.gitea.io/gitea/models/db"
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
unit_model "code.gitea.io/gitea/models/unit"
|
||||
|
@ -836,3 +840,171 @@ func TestUserRepos(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserActivate(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
|
||||
|
||||
called := false
|
||||
code := ""
|
||||
defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) {
|
||||
called = true
|
||||
assert.Len(t, msgs, 1)
|
||||
assert.Equal(t, `"doesnotexist" <doesnotexist@example.com>`, msgs[0].To)
|
||||
assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.activate_account"), msgs[0].Subject)
|
||||
|
||||
messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body)))
|
||||
link, ok := messageDoc.Find("a").Attr("href")
|
||||
assert.True(t, ok)
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
code = u.Query()["code"][0]
|
||||
})()
|
||||
|
||||
session := emptyTestSession(t)
|
||||
req := NewRequestWithValues(t, "POST", "/user/sign_up", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/sign_up"),
|
||||
"user_name": "doesnotexist",
|
||||
"email": "doesnotexist@example.com",
|
||||
"password": "examplePassword!1",
|
||||
"retype": "examplePassword!1",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, called)
|
||||
|
||||
queryCode, err := url.QueryUnescape(code)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(queryCode, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.UserActivation)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator))
|
||||
|
||||
req = NewRequest(t, "POST", "/user/activate?code="+code)
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: "doesnotexist", IsActive: true})
|
||||
}
|
||||
|
||||
func TestUserPasswordReset(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
called := false
|
||||
code := ""
|
||||
defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) {
|
||||
if called {
|
||||
return
|
||||
}
|
||||
called = true
|
||||
|
||||
assert.Len(t, msgs, 1)
|
||||
assert.Equal(t, user2.EmailTo(), msgs[0].To)
|
||||
assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.reset_password"), msgs[0].Subject)
|
||||
|
||||
messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body)))
|
||||
link, ok := messageDoc.Find("a").Attr("href")
|
||||
assert.True(t, ok)
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
code = u.Query()["code"][0]
|
||||
})()
|
||||
|
||||
session := emptyTestSession(t)
|
||||
req := NewRequestWithValues(t, "POST", "/user/forgot_password", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/forgot_password"),
|
||||
"email": user2.Email,
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusOK)
|
||||
assert.True(t, called)
|
||||
|
||||
queryCode, err := url.QueryUnescape(code)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(queryCode, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.PasswordReset)
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/recover_account", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/recover_account"),
|
||||
"code": code,
|
||||
"password": "new_password",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID})
|
||||
assert.True(t, unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}).ValidatePassword("new_password"))
|
||||
}
|
||||
|
||||
func TestActivateEmailAddress(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
defer test.MockVariableValue(&setting.Service.RegisterEmailConfirm, true)()
|
||||
|
||||
user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
|
||||
called := false
|
||||
code := ""
|
||||
defer test.MockVariableValue(&mailer.SendAsync, func(msgs ...*mailer.Message) {
|
||||
if called {
|
||||
return
|
||||
}
|
||||
called = true
|
||||
|
||||
assert.Len(t, msgs, 1)
|
||||
assert.Equal(t, "newemail@example.org", msgs[0].To)
|
||||
assert.EqualValues(t, translation.NewLocale("en-US").Tr("mail.activate_email"), msgs[0].Subject)
|
||||
|
||||
messageDoc := NewHTMLParser(t, bytes.NewBuffer([]byte(msgs[0].Body)))
|
||||
link, ok := messageDoc.Find("a").Attr("href")
|
||||
assert.True(t, ok)
|
||||
u, err := url.Parse(link)
|
||||
require.NoError(t, err)
|
||||
code = u.Query()["code"][0]
|
||||
})()
|
||||
|
||||
session := loginUser(t, user2.Name)
|
||||
req := NewRequestWithValues(t, "POST", "/user/settings/account/email", map[string]string{
|
||||
"_csrf": GetCSRF(t, session, "/user/settings"),
|
||||
"email": "newemail@example.org",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
assert.True(t, called)
|
||||
|
||||
queryCode, err := url.QueryUnescape(code)
|
||||
require.NoError(t, err)
|
||||
|
||||
lookupKey, validator, ok := strings.Cut(queryCode, ":")
|
||||
assert.True(t, ok)
|
||||
|
||||
rawValidator, err := hex.DecodeString(validator)
|
||||
require.NoError(t, err)
|
||||
|
||||
authToken, err := auth_model.FindAuthToken(db.DefaultContext, lookupKey, auth_model.EmailActivation("newemail@example.org"))
|
||||
require.NoError(t, err)
|
||||
assert.False(t, authToken.IsExpired())
|
||||
assert.EqualValues(t, authToken.HashedValidator, auth_model.HashValidator(rawValidator))
|
||||
|
||||
req = NewRequestWithValues(t, "POST", "/user/activate_email", map[string]string{
|
||||
"code": code,
|
||||
"email": "newemail@example.org",
|
||||
})
|
||||
session.MakeRequest(t, req, http.StatusSeeOther)
|
||||
|
||||
unittest.AssertNotExistsBean(t, &auth_model.AuthorizationToken{ID: authToken.ID})
|
||||
unittest.AssertExistsAndLoadBean(t, &user_model.EmailAddress{UID: user2.ID, IsActivated: true, Email: "newemail@example.org"})
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ RUN_MODE = prod
|
|||
[database]
|
||||
DB_TYPE = sqlite3
|
||||
PATH = tests/{{TEST_TYPE}}/gitea-{{TEST_TYPE}}-sqlite/gitea.db
|
||||
SQLITE_JOURNAL_MODE = MEMORY
|
||||
|
||||
[indexer]
|
||||
REPO_INDEXER_ENABLED = true
|
||||
|
|
|
@ -48,6 +48,8 @@ func exitf(format string, args ...any) {
|
|||
os.Exit(1)
|
||||
}
|
||||
|
||||
var preparedDir string
|
||||
|
||||
func InitTest(requireGitea bool) {
|
||||
log.RegisterEventWriter("test", testlogger.NewTestLoggerWriter)
|
||||
|
||||
|
@ -175,6 +177,46 @@ func InitTest(requireGitea bool) {
|
|||
log.Fatal("db.Exec: CREATE SCHEMA: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
case setting.Database.Type.IsSQLite3():
|
||||
setting.Database.Path = ":memory:"
|
||||
}
|
||||
|
||||
setting.Repository.Local.LocalCopyPath = os.TempDir()
|
||||
dir, err := os.MkdirTemp("", "prepared-forgejo")
|
||||
if err != nil {
|
||||
log.Fatal("os.MkdirTemp: %v", err)
|
||||
}
|
||||
preparedDir = dir
|
||||
|
||||
setting.Repository.Local.LocalCopyPath, err = os.MkdirTemp("", "local-upload")
|
||||
if err != nil {
|
||||
log.Fatal("os.MkdirTemp: %v", err)
|
||||
}
|
||||
|
||||
if err := unittest.CopyDir(path.Join(filepath.Dir(setting.AppPath), "tests/gitea-repositories-meta"), dir); err != nil {
|
||||
log.Fatal("os.RemoveAll: %v", err)
|
||||
}
|
||||
ownerDirs, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
log.Fatal("os.ReadDir: %v", err)
|
||||
}
|
||||
|
||||
for _, ownerDir := range ownerDirs {
|
||||
if !ownerDir.Type().IsDir() {
|
||||
continue
|
||||
}
|
||||
repoDirs, err := os.ReadDir(filepath.Join(dir, ownerDir.Name()))
|
||||
if err != nil {
|
||||
log.Fatal("os.ReadDir: %v", err)
|
||||
}
|
||||
for _, repoDir := range repoDirs {
|
||||
_ = os.MkdirAll(filepath.Join(dir, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(dir, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(dir, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(dir, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(dir, ownerDir.Name(), repoDir.Name(), "refs", "pull"), 0o755)
|
||||
}
|
||||
}
|
||||
|
||||
routers.InitWebInstalled(graceful.GetManager().HammerContext())
|
||||
|
@ -225,28 +267,10 @@ func cancelProcesses(t testing.TB, delay time.Duration) {
|
|||
}
|
||||
|
||||
func PrepareGitRepoDirectory(t testing.TB) {
|
||||
require.NoError(t, util.RemoveAll(setting.RepoRootPath))
|
||||
require.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 {
|
||||
require.NoError(t, err, "unable to read the new repo root: %v\n", err)
|
||||
}
|
||||
for _, ownerDir := range ownerDirs {
|
||||
if !ownerDir.Type().IsDir() {
|
||||
continue
|
||||
}
|
||||
repoDirs, err := os.ReadDir(filepath.Join(setting.RepoRootPath, ownerDir.Name()))
|
||||
if err != nil {
|
||||
require.NoError(t, err, "unable to read the new repo root: %v\n", err)
|
||||
}
|
||||
for _, repoDir := range repoDirs {
|
||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "pack"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "objects", "info"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "heads"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "tag"), 0o755)
|
||||
_ = os.MkdirAll(filepath.Join(setting.RepoRootPath, ownerDir.Name(), repoDir.Name(), "refs", "pull"), 0o755)
|
||||
}
|
||||
}
|
||||
var err error
|
||||
setting.RepoRootPath, err = os.MkdirTemp(t.TempDir(), "forgejo-repo-rooth")
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, unittest.CopyDir(preparedDir, setting.RepoRootPath))
|
||||
}
|
||||
|
||||
func PrepareArtifactsStorage(t testing.TB) {
|
||||
|
|
|
@ -56,10 +56,21 @@ export async function renderMermaid() {
|
|||
btn.setAttribute('data-clipboard-text', source);
|
||||
mermaidBlock.append(btn);
|
||||
|
||||
const updateIframeHeight = () => {
|
||||
iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
|
||||
};
|
||||
|
||||
// update height when element's visibility state changes, for example when the diagram is inside
|
||||
// a <details> + <summary> block and the <details> block becomes visible upon user interaction, it
|
||||
// would initially set a incorrect height and the correct height is set during this callback.
|
||||
(new IntersectionObserver(() => {
|
||||
updateIframeHeight();
|
||||
}, {root: document.documentElement})).observe(iframe);
|
||||
|
||||
iframe.addEventListener('load', () => {
|
||||
pre.replaceWith(mermaidBlock);
|
||||
mermaidBlock.classList.remove('tw-hidden');
|
||||
iframe.style.height = `${iframe.contentWindow.document.body.clientHeight}px`;
|
||||
updateIframeHeight();
|
||||
setTimeout(() => { // avoid flash of iframe background
|
||||
mermaidBlock.classList.remove('is-loading');
|
||||
iframe.classList.remove('tw-invisible');
|
||||
|
|
Loading…
Reference in a new issue