Merge remote-tracking branch 'upstream' into forgejo
Some checks failed
Integration tests for the release process / release-simulation (push) Has been cancelled
/ release (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled
Some checks failed
Integration tests for the release process / release-simulation (push) Has been cancelled
/ release (push) Has been cancelled
testing / backend-checks (push) Has been cancelled
testing / frontend-checks (push) Has been cancelled
testing / test-unit (push) Has been cancelled
testing / test-e2e (push) Has been cancelled
testing / test-remote-cacher (redis) (push) Has been cancelled
testing / test-remote-cacher (valkey) (push) Has been cancelled
testing / test-remote-cacher (garnet) (push) Has been cancelled
testing / test-remote-cacher (redict) (push) Has been cancelled
testing / test-mysql (push) Has been cancelled
testing / test-pgsql (push) Has been cancelled
testing / test-sqlite (push) Has been cancelled
testing / security-check (push) Has been cancelled
This commit is contained in:
commit
89613f09a7
31 changed files with 1123 additions and 545 deletions
|
|
@ -28,7 +28,7 @@ jobs:
|
||||||
|
|
||||||
runs-on: docker
|
runs-on: docker
|
||||||
container:
|
container:
|
||||||
image: data.forgejo.org/renovate/renovate:40.40.0
|
image: data.forgejo.org/renovate/renovate:40.48.4
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Load renovate repo cache
|
- name: Load renovate repo cache
|
||||||
|
|
|
||||||
2
Makefile
2
Makefile
|
|
@ -48,7 +48,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour
|
||||||
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go
|
DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.34.0 # renovate: datasource=go
|
||||||
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go
|
GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.5.2 # renovate: datasource=go
|
||||||
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go
|
GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.18.1 # renovate: datasource=go
|
||||||
RENOVATE_NPM_PACKAGE ?= renovate@40.40.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
RENOVATE_NPM_PACKAGE ?= renovate@40.48.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate
|
||||||
|
|
||||||
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
|
# https://github.com/disposable-email-domains/disposable-email-domains/commits/main/
|
||||||
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
|
DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ...
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,7 @@ local addIssueLabelsOverrides(labels) =
|
||||||
regex: '',
|
regex: '',
|
||||||
type: 'query',
|
type: 'query',
|
||||||
multi: true,
|
multi: true,
|
||||||
allValue: '.+'
|
allValue: '.+',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.addTemplate(
|
.addTemplate(
|
||||||
|
|
@ -423,7 +423,7 @@ local addIssueLabelsOverrides(labels) =
|
||||||
regex: '',
|
regex: '',
|
||||||
type: 'query',
|
type: 'query',
|
||||||
multi: true,
|
multi: true,
|
||||||
allValue: '.+'
|
allValue: '.+',
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.addTemplate(
|
.addTemplate(
|
||||||
|
|
|
||||||
|
|
@ -183,7 +183,7 @@ RUN_USER = ; git
|
||||||
;;
|
;;
|
||||||
;; For the built-in SSH server, choose the key exchange algorithms to support for SSH connections,
|
;; For the built-in SSH server, choose the key exchange algorithms to support for SSH connections,
|
||||||
;; for system SSH this setting has no effect
|
;; for system SSH this setting has no effect
|
||||||
;SSH_SERVER_KEY_EXCHANGES = curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1
|
;SSH_SERVER_KEY_EXCHANGES = mlkem768x25519-sha256, curve25519-sha256, ecdh-sha2-nistp256, ecdh-sha2-nistp384, ecdh-sha2-nistp521, diffie-hellman-group14-sha256, diffie-hellman-group14-sha1
|
||||||
;;
|
;;
|
||||||
;; For the built-in SSH server, choose the MACs to support for SSH connections,
|
;; For the built-in SSH server, choose the MACs to support for SSH connections,
|
||||||
;; for system SSH this setting has no effect
|
;; for system SSH this setting has no effect
|
||||||
|
|
|
||||||
2
go.mod
2
go.mod
|
|
@ -17,7 +17,7 @@ require (
|
||||||
code.gitea.io/actions-proto-go v0.4.0
|
code.gitea.io/actions-proto-go v0.4.0
|
||||||
code.gitea.io/sdk/gitea v0.21.0
|
code.gitea.io/sdk/gitea v0.21.0
|
||||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570
|
||||||
connectrpc.com/connect v1.17.0
|
connectrpc.com/connect v1.18.1
|
||||||
github.com/42wim/httpsig v1.2.3
|
github.com/42wim/httpsig v1.2.3
|
||||||
github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920
|
github.com/42wim/sshsig v0.0.0-20250502153856-5100632e8920
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358
|
||||||
|
|
|
||||||
4
go.sum
4
go.sum
|
|
@ -32,8 +32,8 @@ code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4=
|
||||||
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA=
|
||||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
|
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 h1:TXbikPqa7YRtfU9vS6QJBg77pUvbEb6StRdZO8t1bEY=
|
||||||
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
|
codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570/go.mod h1:IIAjsijsd8q1isWX8MACefDEgTQslQ4stk2AeeTt3kM=
|
||||||
connectrpc.com/connect v1.17.0 h1:W0ZqMhtVzn9Zhn2yATuUokDLO5N+gIuBWMOnsQrfmZk=
|
connectrpc.com/connect v1.18.1 h1:PAg7CjSAGvscaf6YZKUefjoih5Z/qYkyaTrBW8xvYPw=
|
||||||
connectrpc.com/connect v1.17.0/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
|
connectrpc.com/connect v1.18.1/go.mod h1:0292hj1rnx8oFrStN7cB4jjVBeqs+Yx5yDIC2prWDO8=
|
||||||
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
|
||||||
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
|
||||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
|
|
|
||||||
|
|
@ -239,3 +239,15 @@
|
||||||
num_members: 2
|
num_members: 2
|
||||||
includes_all_repositories: false
|
includes_all_repositories: false
|
||||||
can_create_org_repo: false
|
can_create_org_repo: false
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 25
|
||||||
|
org_id: 17
|
||||||
|
lower_name: super-user
|
||||||
|
name: super-user
|
||||||
|
description: ""
|
||||||
|
authorize: 3
|
||||||
|
num_repos: 0
|
||||||
|
num_members: 0
|
||||||
|
includes_all_repositories: 0
|
||||||
|
can_create_org_repo: 0
|
||||||
|
|
|
||||||
|
|
@ -329,3 +329,10 @@
|
||||||
team_id: 22
|
team_id: 22
|
||||||
type: 3
|
type: 3
|
||||||
access_mode: 1
|
access_mode: 1
|
||||||
|
|
||||||
|
-
|
||||||
|
id: 84
|
||||||
|
org_id: 17
|
||||||
|
team_id: 25
|
||||||
|
type: 3
|
||||||
|
access_mode: 3
|
||||||
|
|
|
||||||
|
|
@ -642,7 +642,7 @@
|
||||||
num_following: 0
|
num_following: 0
|
||||||
num_stars: 0
|
num_stars: 0
|
||||||
num_repos: 2
|
num_repos: 2
|
||||||
num_teams: 3
|
num_teams: 4
|
||||||
num_members: 4
|
num_members: 4
|
||||||
visibility: 0
|
visibility: 0
|
||||||
repo_admin_change_team_access: false
|
repo_admin_change_team_access: false
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...str
|
||||||
if attach.UUID == "" {
|
if attach.UUID == "" {
|
||||||
return errors.New("attachment uuid should be not blank")
|
return errors.New("attachment uuid should be not blank")
|
||||||
}
|
}
|
||||||
if attach.ExternalURL != "" && !validation.IsValidExternalURL(attach.ExternalURL) {
|
if attach.ExternalURL != "" && !validation.IsValidReleaseAssetURL(attach.ExternalURL) {
|
||||||
return ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
|
return ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
|
||||||
}
|
}
|
||||||
_, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
|
_, err := db.GetEngine(ctx).Where("uuid=?", attach.UUID).Cols(cols...).Update(attach)
|
||||||
|
|
@ -244,7 +244,7 @@ func UpdateAttachmentByUUID(ctx context.Context, attach *Attachment, cols ...str
|
||||||
|
|
||||||
// UpdateAttachment updates the given attachment in database
|
// UpdateAttachment updates the given attachment in database
|
||||||
func UpdateAttachment(ctx context.Context, atta *Attachment) error {
|
func UpdateAttachment(ctx context.Context, atta *Attachment) error {
|
||||||
if atta.ExternalURL != "" && !validation.IsValidExternalURL(atta.ExternalURL) {
|
if atta.ExternalURL != "" && !validation.IsValidReleaseAssetURL(atta.ExternalURL) {
|
||||||
return ErrInvalidExternalURL{ExternalURL: atta.ExternalURL}
|
return ErrInvalidExternalURL{ExternalURL: atta.ExternalURL}
|
||||||
}
|
}
|
||||||
sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count")
|
sess := db.GetEngine(ctx).Cols("name", "issue_id", "release_id", "comment_id", "download_count")
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,8 @@ type ServeHeaderOptions struct {
|
||||||
Filename string
|
Filename string
|
||||||
CacheDuration time.Duration // defaults to 5 minutes
|
CacheDuration time.Duration // defaults to 5 minutes
|
||||||
LastModified time.Time
|
LastModified time.Time
|
||||||
|
AdditionalHeaders http.Header
|
||||||
|
RedirectStatusCode int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeSetHeaders sets necessary content serve headers
|
// ServeSetHeaders sets necessary content serve headers
|
||||||
|
|
@ -82,6 +84,12 @@ func ServeSetHeaders(w http.ResponseWriter, opts *ServeHeaderOptions) {
|
||||||
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
// http.TimeFormat required a UTC time, refer to https://pkg.go.dev/net/http#TimeFormat
|
||||||
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if opts.AdditionalHeaders != nil {
|
||||||
|
for k, v := range opts.AdditionalHeaders {
|
||||||
|
header[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ServeData download file from io.Reader
|
// ServeData download file from io.Reader
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,13 @@ func ParseImageConfig(mt string, r io.Reader) (*Metadata, error) {
|
||||||
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
func parseOCIImageConfig(r io.Reader) (*Metadata, error) {
|
||||||
var image oci.Image
|
var image oci.Image
|
||||||
if err := json.NewDecoder(r).Decode(&image); err != nil {
|
if err := json.NewDecoder(r).Decode(&image); err != nil {
|
||||||
|
// Handle empty config blobs (common in OCI artifacts)
|
||||||
|
if err == io.EOF {
|
||||||
|
return &Metadata{
|
||||||
|
Type: TypeOCI,
|
||||||
|
Platform: DefaultPlatform,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
|
@ -60,3 +61,49 @@ func TestParseImageConfig(t *testing.T) {
|
||||||
assert.Equal(t, projectURL, metadata.ProjectURL)
|
assert.Equal(t, projectURL, metadata.ProjectURL)
|
||||||
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
|
assert.Equal(t, repositoryURL, metadata.RepositoryURL)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseImageConfigEmptyBlob(t *testing.T) {
|
||||||
|
t.Run("Empty config blob (EOF)", func(t *testing.T) {
|
||||||
|
// Test empty reader (simulates empty config blob common in OCI artifacts)
|
||||||
|
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(""))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, TypeOCI, metadata.Type)
|
||||||
|
assert.Equal(t, DefaultPlatform, metadata.Platform)
|
||||||
|
assert.Empty(t, metadata.Description)
|
||||||
|
assert.Empty(t, metadata.Authors)
|
||||||
|
assert.Empty(t, metadata.Labels)
|
||||||
|
assert.Empty(t, metadata.Manifests)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Empty JSON object", func(t *testing.T) {
|
||||||
|
// Test minimal valid JSON config
|
||||||
|
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader("{}"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, TypeOCI, metadata.Type)
|
||||||
|
assert.Equal(t, DefaultPlatform, metadata.Platform)
|
||||||
|
assert.Empty(t, metadata.Description)
|
||||||
|
assert.Empty(t, metadata.Authors)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Invalid JSON still returns error", func(t *testing.T) {
|
||||||
|
// Test that actual JSON errors (not EOF) are still returned
|
||||||
|
_, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader("{invalid json"))
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.NotEqual(t, io.EOF, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("OCI artifact with empty config", func(t *testing.T) {
|
||||||
|
// Test OCI artifact scenario with minimal config
|
||||||
|
configOCI := `{"config": {}}`
|
||||||
|
metadata, err := ParseImageConfig(oci.MediaTypeImageManifest, strings.NewReader(configOCI))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, TypeOCI, metadata.Type)
|
||||||
|
assert.Equal(t, DefaultPlatform, metadata.Platform)
|
||||||
|
assert.Empty(t, metadata.Description)
|
||||||
|
assert.Empty(t, metadata.Authors)
|
||||||
|
assert.Empty(t, metadata.ImageLayers)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ var SSH = struct {
|
||||||
Domain: "",
|
Domain: "",
|
||||||
Port: 22,
|
Port: 22,
|
||||||
ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
|
ServerCiphers: []string{"chacha20-poly1305@openssh.com", "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", "aes256-gcm@openssh.com"},
|
||||||
ServerKeyExchanges: []string{"curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
|
ServerKeyExchanges: []string{"mlkem768x25519-sha256", "curve25519-sha256", "ecdh-sha2-nistp256", "ecdh-sha2-nistp384", "ecdh-sha2-nistp521", "diffie-hellman-group14-sha256", "diffie-hellman-group14-sha1"},
|
||||||
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
|
ServerMACs: []string{"hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1"},
|
||||||
KeygenPath: "",
|
KeygenPath: "",
|
||||||
MinimumKeySizeCheck: true,
|
MinimumKeySizeCheck: true,
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,11 @@ func IsValidExternalURL(uri string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsValidReleaseAssetURL checks if the URL is valid for external release assets
|
||||||
|
func IsValidReleaseAssetURL(uri string) bool {
|
||||||
|
return IsValidURL(uri)
|
||||||
|
}
|
||||||
|
|
||||||
// IsValidExternalTrackerURLFormat checks if URL matches required syntax for external trackers
|
// IsValidExternalTrackerURLFormat checks if URL matches required syntax for external trackers
|
||||||
func IsValidExternalTrackerURLFormat(uri string) bool {
|
func IsValidExternalTrackerURLFormat(uri string) bool {
|
||||||
if !IsValidExternalURL(uri) {
|
if !IsValidExternalURL(uri) {
|
||||||
|
|
|
||||||
|
|
@ -2985,8 +2985,6 @@ teams.invite_team_member.list = Pending invitations
|
||||||
teams.delete_team_title = Delete team
|
teams.delete_team_title = Delete team
|
||||||
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
|
teams.delete_team_desc = Deleting a team revokes repository access from its members. Continue?
|
||||||
teams.delete_team_success = The team has been deleted.
|
teams.delete_team_success = The team has been deleted.
|
||||||
teams.read_permission_desc = This team grants <strong>Read</strong> access: members can view and clone team repositories.
|
|
||||||
teams.write_permission_desc = This team grants <strong>Write</strong> access: members can read from and push to team repositories.
|
|
||||||
teams.admin_permission_desc = This team grants <strong>Administrator</strong> access: members can read from, push to and add collaborators to team repositories.
|
teams.admin_permission_desc = This team grants <strong>Administrator</strong> access: members can read from, push to and add collaborators to team repositories.
|
||||||
teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
|
teams.create_repo_permission_desc = Additionally, this team grants <strong>Create repository</strong> permission: members can create new repositories in organization.
|
||||||
teams.repositories = Team repositories
|
teams.repositories = Team repositories
|
||||||
|
|
|
||||||
928
package-lock.json
generated
928
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -66,7 +66,7 @@
|
||||||
"@stylistic/eslint-plugin-js": "4.4.1",
|
"@stylistic/eslint-plugin-js": "4.4.1",
|
||||||
"@stylistic/stylelint-plugin": "3.1.2",
|
"@stylistic/stylelint-plugin": "3.1.2",
|
||||||
"@vitejs/plugin-vue": "5.2.4",
|
"@vitejs/plugin-vue": "5.2.4",
|
||||||
"@vitest/coverage-v8": "3.2.2",
|
"@vitest/coverage-v8": "3.2.3",
|
||||||
"@vitest/eslint-plugin": "1.2.1",
|
"@vitest/eslint-plugin": "1.2.1",
|
||||||
"@vue/test-utils": "2.4.6",
|
"@vue/test-utils": "2.4.6",
|
||||||
"eslint": "9.28.0",
|
"eslint": "9.28.0",
|
||||||
|
|
@ -76,7 +76,7 @@
|
||||||
"eslint-plugin-no-jquery": "3.1.1",
|
"eslint-plugin-no-jquery": "3.1.1",
|
||||||
"eslint-plugin-no-use-extend-native": "0.7.2",
|
"eslint-plugin-no-use-extend-native": "0.7.2",
|
||||||
"eslint-plugin-playwright": "2.2.0",
|
"eslint-plugin-playwright": "2.2.0",
|
||||||
"eslint-plugin-regexp": "2.8.0",
|
"eslint-plugin-regexp": "2.9.0",
|
||||||
"eslint-plugin-sonarjs": "3.0.2",
|
"eslint-plugin-sonarjs": "3.0.2",
|
||||||
"eslint-plugin-unicorn": "59.0.1",
|
"eslint-plugin-unicorn": "59.0.1",
|
||||||
"eslint-plugin-toml": "0.12.0",
|
"eslint-plugin-toml": "0.12.0",
|
||||||
|
|
@ -87,7 +87,7 @@
|
||||||
"globals": "16.1.0",
|
"globals": "16.1.0",
|
||||||
"happy-dom": "17.6.3",
|
"happy-dom": "17.6.3",
|
||||||
"license-checker-rseidelsohn": "4.4.2",
|
"license-checker-rseidelsohn": "4.4.2",
|
||||||
"markdownlint-cli": "0.44.0",
|
"markdownlint-cli": "0.45.0",
|
||||||
"postcss-html": "1.8.0",
|
"postcss-html": "1.8.0",
|
||||||
"sharp": "0.34.2",
|
"sharp": "0.34.2",
|
||||||
"stylelint": "16.20.0",
|
"stylelint": "16.20.0",
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
"typescript": "5.8.3",
|
"typescript": "5.8.3",
|
||||||
"typescript-eslint": "8.33.1",
|
"typescript-eslint": "8.33.1",
|
||||||
"vite-string-plugin": "1.3.4",
|
"vite-string-plugin": "1.3.4",
|
||||||
"vitest": "3.2.2"
|
"vitest": "3.2.3"
|
||||||
},
|
},
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults"
|
"defaults"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
package container
|
package container
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -62,9 +63,6 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
|
||||||
if h.ContentType != "" {
|
if h.ContentType != "" {
|
||||||
resp.Header().Set("Content-Type", h.ContentType)
|
resp.Header().Set("Content-Type", h.ContentType)
|
||||||
}
|
}
|
||||||
if h.ContentLength != 0 {
|
|
||||||
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
|
|
||||||
}
|
|
||||||
if h.UploadUUID != "" {
|
if h.UploadUUID != "" {
|
||||||
resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
|
resp.Header().Set("Docker-Upload-Uuid", h.UploadUUID)
|
||||||
}
|
}
|
||||||
|
|
@ -72,17 +70,29 @@ func setResponseHeaders(resp http.ResponseWriter, h *containerHeaders) {
|
||||||
resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
|
resp.Header().Set("Docker-Content-Digest", h.ContentDigest)
|
||||||
resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
|
resp.Header().Set("ETag", fmt.Sprintf(`"%s"`, h.ContentDigest))
|
||||||
}
|
}
|
||||||
|
if h.ContentLength >= 0 {
|
||||||
|
resp.Header().Set("Content-Length", strconv.FormatInt(h.ContentLength, 10))
|
||||||
|
}
|
||||||
resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
|
resp.Header().Set("Docker-Distribution-Api-Version", "registry/2.0")
|
||||||
resp.WriteHeader(h.Status)
|
resp.WriteHeader(h.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func jsonResponse(ctx *context.Context, status int, obj any) {
|
func jsonResponse(ctx *context.Context, status int, obj any) {
|
||||||
setResponseHeaders(ctx.Resp, &containerHeaders{
|
// Buffer the JSON content first to calculate correct Content-Length
|
||||||
Status: status,
|
var buf bytes.Buffer
|
||||||
ContentType: "application/json",
|
if err := json.NewEncoder(&buf).Encode(obj); err != nil {
|
||||||
})
|
|
||||||
if err := json.NewEncoder(ctx.Resp).Encode(obj); err != nil {
|
|
||||||
log.Error("JSON encode: %v", err)
|
log.Error("JSON encode: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setResponseHeaders(ctx.Resp, &containerHeaders{
|
||||||
|
Status: status,
|
||||||
|
ContentType: "application/json",
|
||||||
|
ContentLength: int64(buf.Len()),
|
||||||
|
})
|
||||||
|
|
||||||
|
if _, err := buf.WriteTo(ctx.Resp); err != nil {
|
||||||
|
log.Error("JSON write: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -691,33 +701,30 @@ func DeleteManifest(ctx *context.Context) {
|
||||||
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
|
func serveBlob(ctx *context.Context, pfd *packages_model.PackageFileDescriptor) {
|
||||||
serveDirectReqParams := make(url.Values)
|
serveDirectReqParams := make(url.Values)
|
||||||
serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
|
serveDirectReqParams.Set("response-content-type", pfd.Properties.GetByName(container_module.PropertyMediaType))
|
||||||
s, u, _, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
|
s, u, pf, err := packages_service.GetPackageBlobStream(ctx, pfd.File, pfd.Blob, serveDirectReqParams)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if errors.Is(err, packages_model.ErrPackageFileNotExist) {
|
||||||
|
apiError(ctx, http.StatusNotFound, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
headers := &containerHeaders{
|
opts := &context.ServeHeaderOptions{
|
||||||
ContentDigest: pfd.Properties.GetByName(container_module.PropertyDigest),
|
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
|
||||||
ContentType: pfd.Properties.GetByName(container_module.PropertyMediaType),
|
RedirectStatusCode: http.StatusTemporaryRedirect,
|
||||||
ContentLength: pfd.Blob.Size,
|
AdditionalHeaders: map[string][]string{
|
||||||
Status: http.StatusOK,
|
"Docker-Distribution-Api-Version": {"registry/2.0"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if u != nil {
|
if d := pfd.Properties.GetByName(container_module.PropertyDigest); d != "" {
|
||||||
headers.Status = http.StatusTemporaryRedirect
|
opts.AdditionalHeaders["Docker-Content-Digest"] = []string{d}
|
||||||
headers.Location = u.String()
|
opts.AdditionalHeaders["ETag"] = []string{fmt.Sprintf(`"%s"`, d)}
|
||||||
|
|
||||||
setResponseHeaders(ctx.Resp, headers)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer s.Close()
|
helper.ServePackageFile(ctx, s, u, pf, opts)
|
||||||
|
|
||||||
setResponseHeaders(ctx.Resp, headers)
|
|
||||||
if _, err := io.Copy(ctx.Resp, s); err != nil {
|
|
||||||
log.Error("Error whilst copying content to response: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
|
// https://github.com/opencontainers/distribution-spec/blob/main/spec.md#content-discovery
|
||||||
|
|
@ -725,7 +732,7 @@ func GetTagList(ctx *context.Context) {
|
||||||
image := ctx.Params("image")
|
image := ctx.Params("image")
|
||||||
|
|
||||||
if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
|
if _, err := packages_model.GetPackageByName(ctx, ctx.Package.Owner.ID, packages_model.TypeContainer, image); err != nil {
|
||||||
if err == packages_model.ErrPackageNotExist {
|
if errors.Is(err, packages_model.ErrPackageNotExist) {
|
||||||
apiErrorDefined(ctx, errNameUnknown)
|
apiErrorDefined(ctx, errNameUnknown)
|
||||||
} else {
|
} else {
|
||||||
apiError(ctx, http.StatusInternalServerError, err)
|
apiError(ctx, http.StatusInternalServerError, err)
|
||||||
|
|
|
||||||
124
routers/api/packages/container/container_test.go
Normal file
124
routers/api/packages/container/container_test.go
Normal file
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright 2025 The Forgejo Authors. All rights reserved.
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package container
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSetResponseHeaders(t *testing.T) {
|
||||||
|
t.Run("Content-Length for empty content", func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
setResponseHeaders(recorder, &containerHeaders{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
ContentLength: 0, // Empty blob
|
||||||
|
ContentDigest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "0", recorder.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", recorder.Header().Get("Docker-Content-Digest"))
|
||||||
|
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Content-Length for non-empty content", func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
setResponseHeaders(recorder, &containerHeaders{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
ContentLength: 1024,
|
||||||
|
ContentDigest: "sha256:abcd1234",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "1024", recorder.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, "sha256:abcd1234", recorder.Header().Get("Docker-Content-Digest"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("All headers set correctly", func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
setResponseHeaders(recorder, &containerHeaders{
|
||||||
|
Status: http.StatusAccepted,
|
||||||
|
ContentLength: 512,
|
||||||
|
ContentDigest: "sha256:test123",
|
||||||
|
ContentType: "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
Location: "/v2/test/repo/blobs/uploads/uuid123",
|
||||||
|
Range: "0-511",
|
||||||
|
UploadUUID: "uuid123",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "512", recorder.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, "sha256:test123", recorder.Header().Get("Docker-Content-Digest"))
|
||||||
|
assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", recorder.Header().Get("Content-Type"))
|
||||||
|
assert.Equal(t, "/v2/test/repo/blobs/uploads/uuid123", recorder.Header().Get("Location"))
|
||||||
|
assert.Equal(t, "0-511", recorder.Header().Get("Range"))
|
||||||
|
assert.Equal(t, "uuid123", recorder.Header().Get("Docker-Upload-Uuid"))
|
||||||
|
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
|
||||||
|
assert.Equal(t, `"sha256:test123"`, recorder.Header().Get("ETag"))
|
||||||
|
assert.Equal(t, http.StatusAccepted, recorder.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestResponseHeadersForEmptyBlobs tests the core fix for ORAS empty blob support
|
||||||
|
func TestResponseHeadersForEmptyBlobs(t *testing.T) {
|
||||||
|
t.Run("Content-Length set for empty blob", func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
// This tests the main fix: empty blobs should have Content-Length: 0
|
||||||
|
setResponseHeaders(recorder, &containerHeaders{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
ContentLength: 0, // Empty blob (like empty config in ORAS artifacts)
|
||||||
|
ContentDigest: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
})
|
||||||
|
|
||||||
|
// The key fix: Content-Length should be set even for 0-byte blobs
|
||||||
|
assert.Equal(t, "0", recorder.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", recorder.Header().Get("Docker-Content-Digest"))
|
||||||
|
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
|
||||||
|
assert.Equal(t, `"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`, recorder.Header().Get("ETag"))
|
||||||
|
assert.Equal(t, http.StatusOK, recorder.Code)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Content-Length set for regular blob", func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
setResponseHeaders(recorder, &containerHeaders{
|
||||||
|
Status: http.StatusOK,
|
||||||
|
ContentLength: 1024,
|
||||||
|
ContentDigest: "sha256:abcd1234",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "1024", recorder.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, "sha256:abcd1234", recorder.Header().Get("Docker-Content-Digest"))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("All headers set correctly", func(t *testing.T) {
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
|
||||||
|
setResponseHeaders(recorder, &containerHeaders{
|
||||||
|
Status: http.StatusAccepted,
|
||||||
|
ContentLength: 512,
|
||||||
|
ContentDigest: "sha256:test123",
|
||||||
|
ContentType: "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
Location: "/v2/test/repo/blobs/uploads/uuid123",
|
||||||
|
Range: "0-511",
|
||||||
|
UploadUUID: "uuid123",
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.Equal(t, "512", recorder.Header().Get("Content-Length"))
|
||||||
|
assert.Equal(t, "sha256:test123", recorder.Header().Get("Docker-Content-Digest"))
|
||||||
|
assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", recorder.Header().Get("Content-Type"))
|
||||||
|
assert.Equal(t, "/v2/test/repo/blobs/uploads/uuid123", recorder.Header().Get("Location"))
|
||||||
|
assert.Equal(t, "0-511", recorder.Header().Get("Range"))
|
||||||
|
assert.Equal(t, "uuid123", recorder.Header().Get("Docker-Upload-Uuid"))
|
||||||
|
assert.Equal(t, "registry/2.0", recorder.Header().Get("Docker-Distribution-Api-Version"))
|
||||||
|
assert.Equal(t, `"sha256:test123"`, recorder.Header().Get("ETag"))
|
||||||
|
assert.Equal(t, http.StatusAccepted, recorder.Code)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -39,16 +39,9 @@ func LogAndProcessError(ctx *context.Context, status int, obj any, cb func(strin
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serves the content of the package file
|
// ServePackageFile Serves the content of the package file
|
||||||
// If the url is set it will redirect the request, otherwise the content is copied to the response.
|
// If the url is set it will redirect the request, otherwise the content is copied to the response.
|
||||||
func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) {
|
func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf *packages_model.PackageFile, forceOpts ...*context.ServeHeaderOptions) {
|
||||||
if u != nil {
|
|
||||||
ctx.Redirect(u.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
defer s.Close()
|
|
||||||
|
|
||||||
var opts *context.ServeHeaderOptions
|
var opts *context.ServeHeaderOptions
|
||||||
if len(forceOpts) > 0 {
|
if len(forceOpts) > 0 {
|
||||||
opts = forceOpts[0]
|
opts = forceOpts[0]
|
||||||
|
|
@ -59,5 +52,12 @@ func ServePackageFile(ctx *context.Context, s io.ReadSeekCloser, u *url.URL, pf
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if u != nil {
|
||||||
|
ctx.Redirect(u.String(), opts.RedirectStatusCode)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer s.Close()
|
||||||
|
|
||||||
ctx.ServeContent(s, opts)
|
ctx.ServeContent(s, opts)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,7 +51,7 @@ func NewExternalAttachment(ctx context.Context, attach *repo_model.Attachment) (
|
||||||
if attach.ExternalURL == "" {
|
if attach.ExternalURL == "" {
|
||||||
return nil, fmt.Errorf("attachment %s should have a external url", attach.Name)
|
return nil, fmt.Errorf("attachment %s should have a external url", attach.Name)
|
||||||
}
|
}
|
||||||
if !validation.IsValidExternalURL(attach.ExternalURL) {
|
if !validation.IsValidReleaseAssetURL(attach.ExternalURL) {
|
||||||
return nil, repo_model.ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
|
return nil, repo_model.ErrInvalidExternalURL{ExternalURL: attach.ExternalURL}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ func (b *Base) PlainText(status int, text string) {
|
||||||
// Redirect redirects the request
|
// Redirect redirects the request
|
||||||
func (b *Base) Redirect(location string, status ...int) {
|
func (b *Base) Redirect(location string, status ...int) {
|
||||||
code := http.StatusSeeOther
|
code := http.StatusSeeOther
|
||||||
if len(status) == 1 {
|
if len(status) == 1 && status[0] > 0 {
|
||||||
code = status[0]
|
code = status[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ func TestRedirect(t *testing.T) {
|
||||||
cleanup()
|
cleanup()
|
||||||
has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
|
has := resp.Header().Get("Set-Cookie") == "i_like_gitea=dummy"
|
||||||
assert.Equal(t, c.keep, has, "url = %q", c.url)
|
assert.Equal(t, c.keep, has, "url = %q", c.url)
|
||||||
|
assert.Equal(t, http.StatusSeeOther, resp.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
req, _ = http.NewRequest("GET", "/", nil)
|
req, _ = http.NewRequest("GET", "/", nil)
|
||||||
|
|
@ -47,3 +48,24 @@ func TestRedirect(t *testing.T) {
|
||||||
assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
|
assert.Equal(t, "/other", resp.Header().Get("HX-Redirect"))
|
||||||
assert.Equal(t, http.StatusNoContent, resp.Code)
|
assert.Equal(t, http.StatusNoContent, resp.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRedirectOptionalStatus(t *testing.T) {
|
||||||
|
defer test.MockVariableValue(&setting.AppURL, "http://localhost:3000/")()
|
||||||
|
req, _ := http.NewRequest("GET", "/", nil)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
expected int
|
||||||
|
actual int
|
||||||
|
}{
|
||||||
|
{expected: 303},
|
||||||
|
{http.StatusTemporaryRedirect, 307},
|
||||||
|
{http.StatusPermanentRedirect, 308},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
resp := httptest.NewRecorder()
|
||||||
|
b, cleanup := NewBaseContext(resp, req)
|
||||||
|
b.Redirect("/", c.actual)
|
||||||
|
cleanup()
|
||||||
|
assert.Equal(t, c.expected, resp.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,11 +42,8 @@
|
||||||
<li>{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</li>
|
<li>{{ctx.Locale.Tr "org.teams.can_create_org_repo"}}</li>
|
||||||
{{end}}
|
{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
{{if (eq .Team.AccessMode 2)}}
|
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
|
||||||
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
|
{{if (eq .Team.AccessMode 3)}}
|
||||||
{{ctx.Locale.Tr "org.teams.write_permission_desc"}}
|
|
||||||
{{else if (eq .Team.AccessMode 3)}}
|
|
||||||
<h3>{{ctx.Locale.Tr "org.settings.permission"}}</h3>
|
|
||||||
{{ctx.Locale.Tr "org.teams.admin_permission_desc"}}
|
{{ctx.Locale.Tr "org.teams.admin_permission_desc"}}
|
||||||
{{else}}
|
{{else}}
|
||||||
<table class="ui table">
|
<table class="ui table">
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
{{range $release.Attachments}}
|
{{range $release.Attachments}}
|
||||||
{{if .ExternalURL}}
|
{{if .ExternalURL}}
|
||||||
<li>
|
<li>
|
||||||
<a class="tw-flex-1 flex-text-inline tw-font-bold" target="_blank" rel="nofollow" href="{{.DownloadURL}}" download>
|
<a class="tw-flex-1 flex-text-inline tw-font-bold" target="_blank" rel="nofollow" href="{{.DownloadURL}}">
|
||||||
{{svg "octicon-link-external" 16 "tw-mr-1"}}{{.Name}}
|
{{svg "octicon-link-external" 16 "tw-mr-1"}}{{.Name}}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
||||||
116
tests/e2e/org-teams-overview.test.e2e.ts
Normal file
116
tests/e2e/org-teams-overview.test.e2e.ts
Normal file
|
|
@ -0,0 +1,116 @@
|
||||||
|
// @watch start
|
||||||
|
// templates/org/team/sidebar.tmpl
|
||||||
|
// @watch end
|
||||||
|
/* eslint playwright/expect-expect: ["error", { "assertFunctionNames": ["assertPermissionsDetails", "assertRestrictedAccess", "assertOwnerPermissions"] }] */
|
||||||
|
import {expect, type Page} from '@playwright/test';
|
||||||
|
import {test} from './utils_e2e.ts';
|
||||||
|
|
||||||
|
type Permission = 'No access' | 'Write' | 'Read';
|
||||||
|
|
||||||
|
const UNIT_VALUES = [
|
||||||
|
'Code',
|
||||||
|
'Issues',
|
||||||
|
'Pull requests',
|
||||||
|
'Releases',
|
||||||
|
'Wiki',
|
||||||
|
'External Wiki',
|
||||||
|
'External issues',
|
||||||
|
'Projects',
|
||||||
|
'Packages',
|
||||||
|
'Actions',
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type Unit = typeof UNIT_VALUES[number];
|
||||||
|
|
||||||
|
const assertPermission = async (page: Page, name: Unit, permission: Permission) => {
|
||||||
|
await expect.soft(page.getByRole('row', {name}).getByRole('cell').nth(1)).toHaveText(permission);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testTeamUrl = '/org/org17/teams/test_team';
|
||||||
|
const reviewTeamUrl = '/org/org17/teams/review_team';
|
||||||
|
const ownersUrl = '/org/org17/teams/owners';
|
||||||
|
const adminUrl = '/org/org17/teams/super-user';
|
||||||
|
|
||||||
|
const cases: Record<string, { read?: Unit[], write?: Unit[] }> = {
|
||||||
|
[testTeamUrl]: {write: ['Issues']},
|
||||||
|
[reviewTeamUrl]: {read: ['Code']},
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertOwnerPermissions = async (page: Page, code: number = 200) => {
|
||||||
|
const response = await page.goto(ownersUrl);
|
||||||
|
expect(response?.status()).toBe(code);
|
||||||
|
|
||||||
|
await expect(page.getByText('Owners have full access to all repositories and have administrator access to the organization.')).toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertAdminPermissions = async (page: Page, code: number = 200) => {
|
||||||
|
const response = await page.goto(adminUrl);
|
||||||
|
expect(response?.status()).toBe(code);
|
||||||
|
|
||||||
|
await expect(page.getByText('This team grants Administrator access: members can read from, push to and add collaborators to team repositories.')).toBeVisible();
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertRestrictedAccess = async (page: Page, ...urls: string[]) => {
|
||||||
|
for (const url of urls) {
|
||||||
|
expect((await page.goto(url))?.status(), 'should not see any details').toBe(404);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const assertPermissionsDetails = async (page: Page, url: (keyof typeof cases)) => {
|
||||||
|
const response = await page.goto(url);
|
||||||
|
expect(response?.status()).toBe(200);
|
||||||
|
|
||||||
|
const per = cases[url];
|
||||||
|
|
||||||
|
for (const unit of UNIT_VALUES) {
|
||||||
|
if (per.read?.includes(unit)) {
|
||||||
|
await assertPermission(page, unit, 'Read');
|
||||||
|
} else if (per.write?.includes(unit)) {
|
||||||
|
await assertPermission(page, unit, 'Write');
|
||||||
|
} else {
|
||||||
|
await assertPermission(page, unit, 'No access');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('Orga team overview', () => {
|
||||||
|
test.describe('admin', () => {
|
||||||
|
test.use({user: 'user1'});
|
||||||
|
|
||||||
|
test('should see all', async ({page}) => {
|
||||||
|
await assertPermissionsDetails(page, testTeamUrl);
|
||||||
|
await assertPermissionsDetails(page, reviewTeamUrl);
|
||||||
|
await assertOwnerPermissions(page);
|
||||||
|
await assertAdminPermissions(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('owner', () => {
|
||||||
|
test.use({user: 'user18'});
|
||||||
|
|
||||||
|
test('should see all', async ({page}) => {
|
||||||
|
await assertPermissionsDetails(page, testTeamUrl);
|
||||||
|
await assertPermissionsDetails(page, reviewTeamUrl);
|
||||||
|
await assertOwnerPermissions(page);
|
||||||
|
await assertAdminPermissions(page);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('reviewer team', () => {
|
||||||
|
test.use({user: 'user29'});
|
||||||
|
|
||||||
|
test('should only see permissions for `reviewer team` and restricted access to other resources', async ({page}) => {
|
||||||
|
await assertPermissionsDetails(page, reviewTeamUrl);
|
||||||
|
await assertRestrictedAccess(page, ownersUrl, testTeamUrl, adminUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe('test_team', () => {
|
||||||
|
test.use({user: 'user2'});
|
||||||
|
|
||||||
|
test('should only see permissions for test_team and restricted access to other resources', async ({page}) => {
|
||||||
|
await assertPermissionsDetails(page, testTeamUrl);
|
||||||
|
await assertRestrictedAccess(page, ownersUrl, reviewTeamUrl, adminUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -94,6 +94,8 @@ func createSessions(t testing.TB) {
|
||||||
"user1",
|
"user1",
|
||||||
"user2",
|
"user2",
|
||||||
"user12",
|
"user12",
|
||||||
|
"user18",
|
||||||
|
"user29",
|
||||||
"user40",
|
"user40",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,7 @@ func TestPackageContainer(t *testing.T) {
|
||||||
return values
|
return values
|
||||||
}
|
}
|
||||||
|
|
||||||
images := []string{"test", "te/st"}
|
images := []string{"test", "te/st", "oras-artifact"}
|
||||||
tags := []string{"latest", "main"}
|
tags := []string{"latest", "main"}
|
||||||
multiTag := "multi"
|
multiTag := "multi"
|
||||||
|
|
||||||
|
|
@ -177,6 +177,90 @@ func TestPackageContainer(t *testing.T) {
|
||||||
assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version"))
|
assert.Equal(t, "registry/2.0", resp.Header().Get("Docker-Distribution-Api-Version"))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("ORAS Artifact Upload", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
|
image := "oras-artifact"
|
||||||
|
url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image)
|
||||||
|
|
||||||
|
// Empty config blob (common in ORAS artifacts)
|
||||||
|
emptyConfigDigest := "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||||
|
emptyConfigContent := ""
|
||||||
|
|
||||||
|
// Upload empty config blob
|
||||||
|
req := NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, emptyConfigDigest), bytes.NewReader([]byte(emptyConfigContent))).
|
||||||
|
AddTokenAuth(userToken)
|
||||||
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
|
assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, emptyConfigDigest), resp.Header().Get("Location"))
|
||||||
|
assert.Equal(t, emptyConfigDigest, resp.Header().Get("Docker-Content-Digest"))
|
||||||
|
|
||||||
|
// Verify empty blob exists and has correct Content-Length
|
||||||
|
req = NewRequest(t, "HEAD", fmt.Sprintf("%s/blobs/%s", url, emptyConfigDigest)).
|
||||||
|
AddTokenAuth(userToken)
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Equal(t, "0", resp.Header().Get("Content-Length")) // This was the main fix
|
||||||
|
assert.Equal(t, emptyConfigDigest, resp.Header().Get("Docker-Content-Digest"))
|
||||||
|
|
||||||
|
// Upload a small data blob (e.g., artifacthub metadata)
|
||||||
|
artifactData := `{"name":"test-artifact","version":"1.0.0"}`
|
||||||
|
artifactDigest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(artifactData)))
|
||||||
|
|
||||||
|
req = NewRequestWithBody(t, "POST", fmt.Sprintf("%s/blobs/uploads?digest=%s", url, artifactDigest), bytes.NewReader([]byte(artifactData))).
|
||||||
|
AddTokenAuth(userToken)
|
||||||
|
resp = MakeRequest(t, req, http.StatusCreated)
|
||||||
|
assert.Equal(t, fmt.Sprintf("/v2/%s/%s/blobs/%s", user.Name, image, artifactDigest), resp.Header().Get("Location"))
|
||||||
|
|
||||||
|
// Create OCI artifact manifest
|
||||||
|
artifactManifest := fmt.Sprintf(`{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"mediaType": "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
"artifactType": "application/vnd.cncf.artifacthub.config.v1+yaml",
|
||||||
|
"config": {
|
||||||
|
"mediaType": "application/vnd.cncf.artifacthub.config.v1+yaml",
|
||||||
|
"digest": "%s",
|
||||||
|
"size": %d
|
||||||
|
},
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.cncf.artifacthub.repository-metadata.layer.v1.yaml",
|
||||||
|
"digest": "%s",
|
||||||
|
"size": %d
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, emptyConfigDigest, len(emptyConfigContent), artifactDigest, len(artifactData))
|
||||||
|
|
||||||
|
artifactManifestDigest := fmt.Sprintf("sha256:%x", sha256.Sum256([]byte(artifactManifest)))
|
||||||
|
|
||||||
|
// Upload artifact manifest
|
||||||
|
req = NewRequestWithBody(t, "PUT", fmt.Sprintf("%s/manifests/artifact-v1", url), bytes.NewReader([]byte(artifactManifest))).
|
||||||
|
AddTokenAuth(userToken).
|
||||||
|
SetHeader("Content-Type", "application/vnd.oci.image.manifest.v1+json")
|
||||||
|
resp = MakeRequest(t, req, http.StatusCreated)
|
||||||
|
assert.Equal(t, fmt.Sprintf("/v2/%s/%s/manifests/artifact-v1", user.Name, image), resp.Header().Get("Location"))
|
||||||
|
assert.Equal(t, artifactManifestDigest, resp.Header().Get("Docker-Content-Digest"))
|
||||||
|
|
||||||
|
// Verify manifest can be retrieved
|
||||||
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/artifact-v1", url)).
|
||||||
|
AddTokenAuth(userToken).
|
||||||
|
SetHeader("Accept", "application/vnd.oci.image.manifest.v1+json")
|
||||||
|
resp = MakeRequest(t, req, http.StatusOK)
|
||||||
|
assert.Equal(t, "application/vnd.oci.image.manifest.v1+json", resp.Header().Get("Content-Type"))
|
||||||
|
assert.Equal(t, artifactManifestDigest, resp.Header().Get("Docker-Content-Digest"))
|
||||||
|
|
||||||
|
// Verify package was created with correct metadata
|
||||||
|
pvs, err := packages_model.GetVersionsByPackageType(db.DefaultContext, user.ID, packages_model.TypeContainer)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
found := false
|
||||||
|
for _, pv := range pvs {
|
||||||
|
if pv.LowerVersion == "artifact-v1" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found, "ORAS artifact package should be created")
|
||||||
|
})
|
||||||
|
|
||||||
for _, image := range images {
|
for _, image := range images {
|
||||||
t.Run(fmt.Sprintf("[Image:%s]", image), func(t *testing.T) {
|
t.Run(fmt.Sprintf("[Image:%s]", image), func(t *testing.T) {
|
||||||
url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image)
|
url := fmt.Sprintf("%sv2/%s/%s", setting.AppURL, user.Name, image)
|
||||||
|
|
@ -430,14 +514,19 @@ func TestPackageContainer(t *testing.T) {
|
||||||
assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
|
assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("GetManifest", func(t *testing.T) {
|
t.Run("GetManifest unknown-tag", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/unknown-tag", url)).
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/unknown-tag", url)).
|
||||||
AddTokenAuth(userToken)
|
AddTokenAuth(userToken)
|
||||||
MakeRequest(t, req, http.StatusNotFound)
|
MakeRequest(t, req, http.StatusNotFound)
|
||||||
|
})
|
||||||
|
|
||||||
req = NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)).
|
t.Run("GetManifest serv indirect", func(t *testing.T) {
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, false)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)).
|
||||||
AddTokenAuth(userToken)
|
AddTokenAuth(userToken)
|
||||||
resp := MakeRequest(t, req, http.StatusOK)
|
resp := MakeRequest(t, req, http.StatusOK)
|
||||||
|
|
||||||
|
|
@ -446,6 +535,25 @@ func TestPackageContainer(t *testing.T) {
|
||||||
assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
|
assert.Equal(t, manifestDigest, resp.Header().Get("Docker-Content-Digest"))
|
||||||
assert.Equal(t, manifestContent, resp.Body.String())
|
assert.Equal(t, manifestContent, resp.Body.String())
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("GetManifest serv direct", func(t *testing.T) {
|
||||||
|
if setting.Packages.Storage.Type != setting.MinioStorageType {
|
||||||
|
t.Skip("Test skipped for non-Minio-storage.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
defer test.MockVariableValue(&setting.Packages.Storage.MinioConfig.ServeDirect, true)()
|
||||||
|
|
||||||
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/manifests/%s", url, tag)).
|
||||||
|
AddTokenAuth(userToken)
|
||||||
|
resp := MakeRequest(t, req, http.StatusTemporaryRedirect)
|
||||||
|
|
||||||
|
assert.Empty(t, resp.Header().Get("Content-Length"))
|
||||||
|
assert.NotEmpty(t, resp.Header().Get("Location"))
|
||||||
|
assert.Equal(t, "text/html; charset=utf-8", resp.Header().Get("Content-Type"))
|
||||||
|
assert.Empty(t, resp.Header().Get("Docker-Content-Digest"))
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -580,36 +688,76 @@ func TestPackageContainer(t *testing.T) {
|
||||||
t.Run("GetTagList", func(t *testing.T) {
|
t.Run("GetTagList", func(t *testing.T) {
|
||||||
defer tests.PrintCurrentTest(t)()
|
defer tests.PrintCurrentTest(t)()
|
||||||
|
|
||||||
cases := []struct {
|
var cases []struct {
|
||||||
URL string
|
URL string
|
||||||
ExpectedTags []string
|
ExpectedTags []string
|
||||||
ExpectedLink string
|
ExpectedLink string
|
||||||
}{
|
}
|
||||||
{
|
|
||||||
URL: fmt.Sprintf("%s/tags/list", url),
|
if image == "oras-artifact" {
|
||||||
ExpectedTags: []string{"latest", "main", "multi"},
|
cases = []struct {
|
||||||
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
|
URL string
|
||||||
},
|
ExpectedTags []string
|
||||||
{
|
ExpectedLink string
|
||||||
URL: fmt.Sprintf("%s/tags/list?n=0", url),
|
}{
|
||||||
ExpectedTags: []string{},
|
{
|
||||||
ExpectedLink: "",
|
URL: fmt.Sprintf("%s/tags/list", url),
|
||||||
},
|
ExpectedTags: []string{"artifact-v1", "latest", "main", "multi"},
|
||||||
{
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
|
||||||
URL: fmt.Sprintf("%s/tags/list?n=2", url),
|
},
|
||||||
ExpectedTags: []string{"latest", "main"},
|
{
|
||||||
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=2>; rel="next"`, user.Name, image),
|
URL: fmt.Sprintf("%s/tags/list?n=0", url),
|
||||||
},
|
ExpectedTags: []string{},
|
||||||
{
|
ExpectedLink: "",
|
||||||
URL: fmt.Sprintf("%s/tags/list?last=main", url),
|
},
|
||||||
ExpectedTags: []string{"multi"},
|
{
|
||||||
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
|
URL: fmt.Sprintf("%s/tags/list?n=2", url),
|
||||||
},
|
ExpectedTags: []string{"artifact-v1", "latest"},
|
||||||
{
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=latest&n=2>; rel="next"`, user.Name, image),
|
||||||
URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
|
},
|
||||||
ExpectedTags: []string{"main"},
|
{
|
||||||
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
|
URL: fmt.Sprintf("%s/tags/list?last=main", url),
|
||||||
},
|
ExpectedTags: []string{"multi"},
|
||||||
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
|
||||||
|
ExpectedTags: []string{"main"},
|
||||||
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cases = []struct {
|
||||||
|
URL string
|
||||||
|
ExpectedTags []string
|
||||||
|
ExpectedLink string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
URL: fmt.Sprintf("%s/tags/list", url),
|
||||||
|
ExpectedTags: []string{"latest", "main", "multi"},
|
||||||
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: fmt.Sprintf("%s/tags/list?n=0", url),
|
||||||
|
ExpectedTags: []string{},
|
||||||
|
ExpectedLink: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: fmt.Sprintf("%s/tags/list?n=2", url),
|
||||||
|
ExpectedTags: []string{"latest", "main"},
|
||||||
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=2>; rel="next"`, user.Name, image),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: fmt.Sprintf("%s/tags/list?last=main", url),
|
||||||
|
ExpectedTags: []string{"multi"},
|
||||||
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=multi>; rel="next"`, user.Name, image),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
URL: fmt.Sprintf("%s/tags/list?n=1&last=latest", url),
|
||||||
|
ExpectedTags: []string{"main"},
|
||||||
|
ExpectedLink: fmt.Sprintf(`</v2/%s/%s/tags/list?last=main&n=1>; rel="next"`, user.Name, image),
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
|
|
@ -636,7 +784,11 @@ func TestPackageContainer(t *testing.T) {
|
||||||
|
|
||||||
var apiPackages []*api.Package
|
var apiPackages []*api.Package
|
||||||
DecodeJSON(t, resp, &apiPackages)
|
DecodeJSON(t, resp, &apiPackages)
|
||||||
assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..."
|
if image == "oras-artifact" {
|
||||||
|
assert.Len(t, apiPackages, 5) // "artifact-v1", "latest", "main", "multi", "sha256:..."
|
||||||
|
} else {
|
||||||
|
assert.Len(t, apiPackages, 4) // "latest", "main", "multi", "sha256:..."
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Delete", func(t *testing.T) {
|
t.Run("Delete", func(t *testing.T) {
|
||||||
|
|
|
||||||
|
|
@ -430,6 +430,30 @@ func TestAPIExternalAssetRelease(t *testing.T) {
|
||||||
assert.Equal(t, "external", attachment.Type)
|
assert.Equal(t, "external", attachment.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAPIAllowedAPIURLInRelease(t *testing.T) {
|
||||||
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
r := createNewReleaseUsingAPI(t, token, owner, repo, "release-tag", "", "Release Tag", "test")
|
||||||
|
internalURL := "https://localhost:3003/api/packages/owner/generic/test/1.0.0/test.txt"
|
||||||
|
|
||||||
|
req := NewRequest(t, http.MethodPost, fmt.Sprintf("/api/v1/repos/%s/%s/releases/%d/assets?name=test-asset&external_url=%s", owner.Name, repo.Name, r.ID, url.QueryEscape(internalURL))).
|
||||||
|
AddTokenAuth(token)
|
||||||
|
resp := MakeRequest(t, req, http.StatusCreated)
|
||||||
|
|
||||||
|
var attachment *api.Attachment
|
||||||
|
DecodeJSON(t, resp, &attachment)
|
||||||
|
|
||||||
|
assert.Equal(t, "test-asset", attachment.Name)
|
||||||
|
assert.EqualValues(t, 0, attachment.Size)
|
||||||
|
assert.Equal(t, internalURL, attachment.DownloadURL)
|
||||||
|
assert.Equal(t, "external", attachment.Type)
|
||||||
|
}
|
||||||
|
|
||||||
func TestAPIDuplicateAssetRelease(t *testing.T) {
|
func TestAPIDuplicateAssetRelease(t *testing.T) {
|
||||||
defer tests.PrepareTestEnv(t)()
|
defer tests.PrepareTestEnv(t)()
|
||||||
|
|
||||||
|
|
|
||||||
28
web_src/fomantic/package-lock.json
generated
28
web_src/fomantic/package-lock.json
generated
|
|
@ -494,9 +494,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.21",
|
"version": "22.15.30",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.21.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
|
||||||
"integrity": "sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==",
|
"integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
|
|
@ -1117,9 +1117,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/browserslist": {
|
"node_modules/browserslist": {
|
||||||
"version": "4.24.5",
|
"version": "4.25.0",
|
||||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz",
|
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz",
|
||||||
"integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==",
|
"integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -1136,8 +1136,8 @@
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"caniuse-lite": "^1.0.30001716",
|
"caniuse-lite": "^1.0.30001718",
|
||||||
"electron-to-chromium": "^1.5.149",
|
"electron-to-chromium": "^1.5.160",
|
||||||
"node-releases": "^2.0.19",
|
"node-releases": "^2.0.19",
|
||||||
"update-browserslist-db": "^1.1.3"
|
"update-browserslist-db": "^1.1.3"
|
||||||
},
|
},
|
||||||
|
|
@ -1249,9 +1249,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/caniuse-lite": {
|
"node_modules/caniuse-lite": {
|
||||||
"version": "1.0.30001718",
|
"version": "1.0.30001721",
|
||||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz",
|
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
|
||||||
"integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==",
|
"integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
|
@ -2005,9 +2005,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.157",
|
"version": "1.5.165",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.157.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.165.tgz",
|
||||||
"integrity": "sha512-/0ybgsQd1muo8QlnuTpKwtl0oX5YMlUGbm8xyqgDU00motRkKFFbUJySAQBWcY79rVqNLWIWa87BGVGClwAB2w==",
|
"integrity": "sha512-naiMx1Z6Nb2TxPU6fiFrUrDTjyPMLdTtaOd2oLmG8zVSg2hCWGkhPyxwk+qRmZ1ytwVqUv0u7ZcDA5+ALhaUtw==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/emoji-regex": {
|
"node_modules/emoji-regex": {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue