mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2026-05-12 22:10:25 +00:00
**Backport:** https://codeberg.org/forgejo/forgejo/pulls/11776 Fixes #11438. Whenever a "unique constraint violation" error is encountered by package mutation, detect if a `xorm.ErrUniqueConstraintViolation` error occurs. If it does, retry the entire transaction. ## Checklist The [contributor guide](https://forgejo.org/docs/next/contributor/) contains information that will be helpful to first time contributors. There also are a few [conditions for merging Pull Requests in Forgejo repositories](https://codeberg.org/forgejo/governance/src/branch/main/PullRequestsAgreement.md). You are also welcome to join the [Forgejo development chatroom](https://matrix.to/#/#forgejo-development:matrix.org). ### Tests for Go changes (can be removed for JavaScript changes) - I added test coverage for Go changes... - [ ] in their respective `*_test.go` for unit tests. - [ ] in the `tests/integration` directory if it involves interactions with a live Forgejo server. - I ran... - [ ] `make pr-go` before pushing ### Documentation - [ ] I created a pull request [to the documentation](https://codeberg.org/forgejo/docs) to explain to Forgejo users how to use this change. - [x] I did not document these changes and I do not expect someone else to do it. ### Release notes - [x] This change will be noticed by a Forgejo user or admin (feature, bug fix, performance, etc.). I suggest to include a release note for this change. - [ ] This change is not visible to a Forgejo user or admin (refactor, dependency upgrade, etc.). I think there is no need to add a release note for this change. Co-authored-by: Mathieu Fenniak <mathieu@fenniak.net> Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/11833 Reviewed-by: Mathieu Fenniak <mfenniak@noreply.codeberg.org> Co-authored-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org> Co-committed-by: forgejo-backport-action <forgejo-backport-action@noreply.codeberg.org>
359 lines
13 KiB
Go
359 lines
13 KiB
Go
// Copyright 2023 The Gitea Authors. All rights reserved.
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package integration
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"forgejo.org/models/db"
|
|
"forgejo.org/models/packages"
|
|
"forgejo.org/models/unittest"
|
|
user_model "forgejo.org/models/user"
|
|
"forgejo.org/modules/base"
|
|
debian_module "forgejo.org/modules/packages/debian"
|
|
"forgejo.org/modules/setting"
|
|
"forgejo.org/tests"
|
|
|
|
"github.com/blakesmith/ar"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func createDebianArchive(name, version, architecture, packageDescription string) io.Reader {
|
|
var cbuf bytes.Buffer
|
|
zw := gzip.NewWriter(&cbuf)
|
|
tw := tar.NewWriter(zw)
|
|
tw.WriteHeader(&tar.Header{
|
|
Name: "control",
|
|
Mode: 0o600,
|
|
Size: 50,
|
|
})
|
|
fmt.Fprintf(tw, "Package: %s\nVersion: %s\nArchitecture: %s\nDescription: %s\n", name, version, architecture, packageDescription)
|
|
tw.Close()
|
|
zw.Close()
|
|
|
|
var buf bytes.Buffer
|
|
aw := ar.NewWriter(&buf)
|
|
aw.WriteGlobalHeader()
|
|
hdr := &ar.Header{
|
|
Name: "control.tar.gz",
|
|
Mode: 0o600,
|
|
Size: int64(cbuf.Len()),
|
|
}
|
|
aw.WriteHeader(hdr)
|
|
aw.Write(cbuf.Bytes())
|
|
return &buf
|
|
}
|
|
|
|
func TestPackageDebian(t *testing.T) {
|
|
defer tests.PrepareTestEnv(t)()
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
packageName := "gitea"
|
|
packageVersion := "1.0.3"
|
|
packageVersion2 := "1.0.4"
|
|
packageDescription := "Package Description"
|
|
|
|
distributions := []string{"test", "gitea"}
|
|
components := []string{"main", "stable"}
|
|
architectures := []string{"all", "amd64"}
|
|
|
|
rootURL := fmt.Sprintf("/api/packages/%s/debian", user.Name)
|
|
|
|
t.Run("RepositoryKey", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
req := NewRequest(t, "HEAD", rootURL+"/repository.key")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequest(t, "GET", rootURL+"/repository.key")
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
assert.Equal(t, "application/pgp-keys", resp.Header().Get("Content-Type"))
|
|
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP PUBLIC KEY BLOCK-----")
|
|
})
|
|
|
|
for _, distribution := range distributions {
|
|
t.Run(fmt.Sprintf("[Distribution:%s]", distribution), func(t *testing.T) {
|
|
for _, component := range components {
|
|
for _, architecture := range architectures {
|
|
t.Run(fmt.Sprintf("[Component:%s,Architecture:%s]", component, architecture), func(t *testing.T) {
|
|
uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, distribution, component)
|
|
|
|
t.Run("Upload", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
req := NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{}))
|
|
MakeRequest(t, req, http.StatusUnauthorized)
|
|
|
|
req = NewRequestWithBody(t, "PUT", uploadURL, bytes.NewReader([]byte{})).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
|
|
req = NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive("", "", "", packageDescription)).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
|
|
req = NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion, architecture, packageDescription)).
|
|
AddBasicAuth(user.Name).
|
|
SetHeader("content-type", "multipart/form-data")
|
|
MakeRequest(t, req, http.StatusBadRequest)
|
|
|
|
req = NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion, architecture, packageDescription)).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
|
|
pv, err := packages.GetVersionByNameAndVersion(db.DefaultContext, user.ID, packages.TypeDebian, packageName, packageVersion)
|
|
require.NoError(t, err)
|
|
|
|
pd, err := packages.GetPackageDescriptor(db.DefaultContext, pv)
|
|
require.NoError(t, err)
|
|
assert.Nil(t, pd.SemVer)
|
|
assert.IsType(t, &debian_module.Metadata{}, pd.Metadata)
|
|
assert.Equal(t, packageName, pd.Package.Name)
|
|
assert.Equal(t, packageVersion, pd.Version.Version)
|
|
|
|
pfs, err := packages.GetFilesByVersionID(db.DefaultContext, pv.ID)
|
|
require.NoError(t, err)
|
|
assert.NotEmpty(t, pfs)
|
|
assert.Condition(t, func() bool {
|
|
seen := false
|
|
expectedFilename := fmt.Sprintf("%s_%s_%s.deb", packageName, packageVersion, architecture)
|
|
expectedCompositeKey := fmt.Sprintf("%s|%s", distribution, component)
|
|
for _, pf := range pfs {
|
|
if pf.Name == expectedFilename && pf.CompositeKey == expectedCompositeKey {
|
|
if seen {
|
|
return false
|
|
}
|
|
seen = true
|
|
|
|
assert.True(t, pf.IsLead)
|
|
|
|
pfps, err := packages.GetProperties(db.DefaultContext, packages.PropertyTypeFile, pf.ID)
|
|
require.NoError(t, err)
|
|
|
|
for _, pfp := range pfps {
|
|
switch pfp.Name {
|
|
case debian_module.PropertyDistribution:
|
|
assert.Equal(t, distribution, pfp.Value)
|
|
case debian_module.PropertyComponent:
|
|
assert.Equal(t, component, pfp.Value)
|
|
case debian_module.PropertyArchitecture:
|
|
assert.Equal(t, architecture, pfp.Value)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return seen
|
|
})
|
|
|
|
req = NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion, architecture, packageDescription)).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusConflict)
|
|
})
|
|
|
|
t.Run("Download", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/pool/%s/%s/%s_%s_%s.deb", rootURL, distribution, component, packageName, packageVersion, architecture))
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
assert.Equal(t, "application/vnd.debian.binary-package", resp.Header().Get("Content-Type"))
|
|
})
|
|
|
|
t.Run("Packages", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
req := NewRequestWithBody(t, "PUT", uploadURL, createDebianArchive(packageName, packageVersion2, architecture, packageDescription)).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
|
|
url := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture)
|
|
|
|
req = NewRequest(t, "GET", url)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
body := resp.Body.String()
|
|
|
|
assert.Contains(t, body, "Package: "+packageName+"\n")
|
|
assert.Contains(t, body, "Version: "+packageVersion+"\n")
|
|
assert.Contains(t, body, "Version: "+packageVersion2+"\n")
|
|
assert.Contains(t, body, "Architecture: "+architecture+"\n")
|
|
assert.Contains(t, body, fmt.Sprintf("Filename: pool/%s/%s/%s_%s_%s.deb\n", distribution, component, packageName, packageVersion, architecture))
|
|
assert.Contains(t, body, fmt.Sprintf("Filename: pool/%s/%s/%s_%s_%s.deb\n", distribution, component, packageName, packageVersion2, architecture))
|
|
|
|
req = NewRequest(t, "GET", url+".gz")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequest(t, "GET", url+".xz")
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
url = fmt.Sprintf("%s/dists/%s/%s/%s/by-hash/SHA256/%s", rootURL, distribution, component, architecture, base.EncodeSha256(body))
|
|
req = NewRequest(t, "GET", url)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
assert.Equal(t, body, resp.Body.String())
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
t.Run("Release", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
req := NewRequest(t, "HEAD", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution))
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution))
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
body := resp.Body.String()
|
|
|
|
assert.Contains(t, body, "Components: "+strings.Join(components, " ")+"\n")
|
|
assert.Contains(t, body, "Architectures: "+strings.Join(architectures, " ")+"\n")
|
|
|
|
for _, component := range components {
|
|
for _, architecture := range architectures {
|
|
assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages\n", component, architecture))
|
|
assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages.gz\n", component, architecture))
|
|
assert.Contains(t, body, fmt.Sprintf("%s/binary-%s/Packages.xz\n", component, architecture))
|
|
}
|
|
}
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/by-hash/SHA256/%s", rootURL, distribution, base.EncodeSha256(body)))
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
assert.Equal(t, body, resp.Body.String())
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release.gpg", rootURL, distribution))
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNATURE-----")
|
|
|
|
req = NewRequest(t, "HEAD", fmt.Sprintf("%s/dists/%s/InRelease", rootURL, distribution))
|
|
MakeRequest(t, req, http.StatusOK)
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/InRelease", rootURL, distribution))
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
|
|
assert.Contains(t, resp.Body.String(), "-----BEGIN PGP SIGNED MESSAGE-----")
|
|
})
|
|
})
|
|
}
|
|
|
|
t.Run("Delete", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
distribution := distributions[0]
|
|
architecture := architectures[0]
|
|
|
|
for _, component := range components {
|
|
req := NewRequest(t, "DELETE", fmt.Sprintf("%s/pool/%s/%s/%s/%s/%s", rootURL, distribution, component, packageName, packageVersion, architecture))
|
|
MakeRequest(t, req, http.StatusUnauthorized)
|
|
|
|
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/pool/%s/%s/%s/%s/%s", rootURL, distribution, component, packageName, packageVersion, architecture)).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
|
|
req = NewRequest(t, "DELETE", fmt.Sprintf("%s/pool/%s/%s/%s/%s/%s", rootURL, distribution, component, packageName, packageVersion2, architecture)).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusNoContent)
|
|
|
|
req = NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture))
|
|
MakeRequest(t, req, http.StatusNotFound)
|
|
}
|
|
|
|
req := NewRequest(t, "GET", fmt.Sprintf("%s/dists/%s/Release", rootURL, distribution))
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
|
|
body := resp.Body.String()
|
|
|
|
assert.Contains(t, body, "Components: "+strings.Join(components, " ")+"\n")
|
|
assert.Contains(t, body, "Architectures: "+architectures[1]+"\n")
|
|
})
|
|
|
|
t.Run("Delete via UI", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
// Test precondition -- ensure that the packageVersion & packageVersion2 are listed in the index
|
|
indexURL := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distributions[1], components[0], architectures[0])
|
|
req := NewRequest(t, "GET", indexURL)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
body := resp.Body.String()
|
|
require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion))
|
|
require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion2))
|
|
|
|
// Perform backend request that simulates the "Delete package" UI option, which is a generic codepath without
|
|
// debian-specific package management awareness...
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
session := loginUser(t, user.Name)
|
|
settingsURL := fmt.Sprintf("/user2/-/packages/debian/%s/%s/settings", packageName, packageVersion)
|
|
req = NewRequestWithValues(t, "POST", settingsURL, map[string]string{
|
|
"action": "delete",
|
|
})
|
|
session.MakeRequest(t, req, http.StatusSeeOther)
|
|
|
|
// Ensure that the package index has been rebuilt without the deleted package, handled by debianPackageNotifier
|
|
req = NewRequest(t, "GET", indexURL)
|
|
resp = MakeRequest(t, req, http.StatusOK)
|
|
body = resp.Body.String()
|
|
assert.NotContains(t, body, fmt.Sprintf("Version: %s", packageVersion))
|
|
require.Contains(t, body, fmt.Sprintf("Version: %s", packageVersion2))
|
|
})
|
|
}
|
|
|
|
func TestPackageDebianConcurrent(t *testing.T) {
|
|
if setting.Database.Type.IsSQLite3() {
|
|
// Concurrency test fails on SQLite w/ "database is locked"
|
|
t.Skip()
|
|
}
|
|
|
|
defer tests.PrepareTestEnv(t)()
|
|
|
|
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
|
|
|
distribution := "test"
|
|
component := "main"
|
|
architecture := "amd64"
|
|
packageName := "gitea"
|
|
packageDescription := "Package Description"
|
|
|
|
rootURL := fmt.Sprintf("/api/packages/%s/debian", user.Name)
|
|
uploadURL := fmt.Sprintf("%s/pool/%s/%s/upload", rootURL, distribution, component)
|
|
|
|
t.Run("Concurrent Upload", func(t *testing.T) {
|
|
defer tests.PrintCurrentTest(t)()
|
|
|
|
var wg sync.WaitGroup
|
|
packageCount := 10
|
|
for i := range packageCount {
|
|
wg.Go(func() {
|
|
req := NewRequestWithBody(t, "PUT", uploadURL,
|
|
createDebianArchive(packageName, fmt.Sprintf("1.0.%d", i), architecture, packageDescription)).
|
|
AddBasicAuth(user.Name)
|
|
MakeRequest(t, req, http.StatusCreated)
|
|
})
|
|
}
|
|
wg.Wait()
|
|
|
|
url := fmt.Sprintf("%s/dists/%s/%s/binary-%s/Packages", rootURL, distribution, component, architecture)
|
|
|
|
req := NewRequest(t, "GET", url)
|
|
resp := MakeRequest(t, req, http.StatusOK)
|
|
body := resp.Body.String()
|
|
|
|
assert.Contains(t, body, fmt.Sprintf("Package: %s\n", packageName))
|
|
for i := range packageCount {
|
|
assert.Contains(t, body, fmt.Sprintf("Version: 1.0.%d\n", i))
|
|
}
|
|
})
|
|
}
|