Files
Gitea/modules/packages/arch/metadata_test.go
T
philip-x-rutkowski-intel-com 6487d14855 enhance(packages/npm): expand version metadata and support npm deprecate (#37890)
Fixes #21624

Adds the npm package version metadata fields that Gitea's npm registry
was previously dropping on publish, and implements the `npm deprecate`
command, which Gitea did not accept before.

### New / pass-through metadata fields

The following are now parsed from the publish payload, persisted in the
stored `npm.Metadata`, and re-emitted on the abbreviated version
manifest returned to npm clients:

- `hasInstallScript` — auto-detected from `scripts.preinstall` /
`scripts.install` / `scripts.postinstall` (also honors a client-supplied
value). Without this flag, `npm install` skips lifecycle scripts.
- `_hasShrinkwrap` — authoritatively derived by inspecting the uploaded
tarball for a top-level `*/npm-shrinkwrap.json` entry. Client-supplied
values are ignored. Decompression failures fall back to `false` and do
not block publish (integrity has already been validated).
- `engines` (`map[string]string`)
- `cpu`, `os` (`[]string`)
- `directories` (`map[string]string`)
- `funding` (`any`; preserves the spec's string / object / array shape)
- `acceptDependencies` (`map[string]string`)
- `deprecated` (`string`)

`peerDependenciesMeta` was already in the stored struct but is now
exercised
by tests.

### `npm deprecate` support

`npm deprecate <pkg-spec> <message>` PUTs the package document to the
same URL as publish but with no `_attachments`. The router now detects
that shape and routes to a new handler that updates each affected
version's stored `Metadata.Deprecated` via
`packages_model.UpdateVersion`. An empty message clears the flag
(undeprecate). Unknown versions are silently skipped, matching npm's
behavior. No new routes were added.

Supported invocations include:

- `npm deprecate my-thing@"< 0.2.3" "critical bug fixed in v0.2.3"`
- `npm deprecate my-thing@1.x "1.x is no longer supported"`
- `npm deprecate my-thing@1.0.0 ""` (undeprecate)

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
Co-authored-by: bircni <bircni@icloud.com>
2026-08-03 05:26:08 +00:00

195 lines
5.5 KiB
Go

// Copyright 2023 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package arch
import (
"archive/tar"
"bytes"
"compress/gzip"
"io"
"testing"
"gitea.dev/modules/test"
"gitea.dev/modules/util"
"github.com/klauspost/compress/zstd"
"github.com/stretchr/testify/assert"
"github.com/ulikunitz/xz"
)
const (
packageName = "gitea"
packageVersion = "1.0.1"
packageDescription = "Package Description"
packageProjectURL = "https://gitea.com"
packagePackager = "KN4CK3R <packager@gitea.com>"
)
func createPKGINFOContent(name, version string) []byte {
return []byte(`pkgname = ` + name + `
pkgbase = ` + name + `
pkgver = ` + version + `
pkgdesc = ` + packageDescription + `
url = ` + packageProjectURL + `
# comment
group=group
builddate = 1678834800
size = 123456
arch = x86_64
license = MIT
packager = ` + packagePackager + `
depend = common
xdata = value
depend = gitea
provides = common
provides = gitea
optdepend = hex
replaces = gogs
checkdepend = common
makedepend = cmake
conflict = ninja
backup = usr/bin/paket1`)
}
func TestParsePackage(t *testing.T) {
createPackage := func(compression string, files map[string][]byte) io.Reader {
var buf bytes.Buffer
var cw io.WriteCloser
switch compression {
case "zst":
cw, _ = zstd.NewWriter(&buf)
case "xz":
cw, _ = xz.NewWriter(&buf)
case "gz":
cw = gzip.NewWriter(&buf)
}
tw := tar.NewWriter(cw)
for name, content := range files {
hdr := &tar.Header{
Name: name,
Mode: 0o600,
Size: int64(len(content)),
}
tw.WriteHeader(hdr)
tw.Write(content)
}
tw.Close()
cw.Close()
return &buf
}
for _, c := range []string{"gz", "xz", "zst"} {
t.Run(c, func(t *testing.T) {
t.Run("MissingPKGINFOFile", func(t *testing.T) {
data := createPackage(c, map[string][]byte{"dummy.txt": {}})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrMissingPKGINFOFile)
})
t.Run("InvalidPKGINFOFile", func(t *testing.T) {
data := createPackage(c, map[string][]byte{".PKGINFO": {}})
pp, err := ParsePackage(data)
assert.Nil(t, pp)
assert.ErrorIs(t, err, ErrInvalidName)
})
t.Run("Valid", func(t *testing.T) {
data := createPackage(c, map[string][]byte{
".PKGINFO": createPKGINFOContent(packageName, packageVersion),
"/test/dummy.txt": {},
})
p, err := ParsePackage(data)
assert.NoError(t, err)
assert.NotNil(t, p)
assert.ElementsMatch(t, []string{"/test/dummy.txt"}, p.FileMetadata.Files)
})
})
}
}
func TestParsePackageInfo(t *testing.T) {
t.Run("InvalidName", func(t *testing.T) {
data := createPKGINFOContent("", packageVersion)
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidName)
})
t.Run("Regexp", func(t *testing.T) {
assert.Regexp(t, versionPattern, "1.2_3~4+5")
assert.Regexp(t, versionPattern, "1:2_3~4+5")
assert.NotRegexp(t, versionPattern, "a:1.0.0-1")
assert.NotRegexp(t, versionPattern, "0.0.1/1-1")
assert.NotRegexp(t, versionPattern, "1.0.0 -1")
})
t.Run("InvalidVersion", func(t *testing.T) {
data := createPKGINFOContent(packageName, "")
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidVersion)
})
t.Run("Valid", func(t *testing.T) {
data := createPKGINFOContent(packageName, packageVersion)
p, err := ParsePackageInfo(bytes.NewReader(data))
assert.NoError(t, err)
assert.NotNil(t, p)
assert.Equal(t, packageName, p.Name)
assert.Equal(t, packageName, p.FileMetadata.Base)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, packageDescription, p.VersionMetadata.Description)
assert.Equal(t, packagePackager, p.FileMetadata.Packager)
assert.Equal(t, packageProjectURL, p.VersionMetadata.ProjectURL)
assert.ElementsMatch(t, []string{"MIT"}, p.VersionMetadata.Licenses)
assert.EqualValues(t, 1678834800, p.FileMetadata.BuildDate)
assert.EqualValues(t, 123456, p.FileMetadata.InstalledSize)
assert.Equal(t, "x86_64", p.FileMetadata.Architecture)
assert.ElementsMatch(t, []string{"value"}, p.FileMetadata.XData)
assert.ElementsMatch(t, []string{"group"}, p.FileMetadata.Groups)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Provides)
assert.ElementsMatch(t, []string{"common", "gitea"}, p.FileMetadata.Depends)
assert.ElementsMatch(t, []string{"gogs"}, p.FileMetadata.Replaces)
assert.ElementsMatch(t, []string{"hex"}, p.FileMetadata.OptDepends)
assert.ElementsMatch(t, []string{"common"}, p.FileMetadata.CheckDepends)
assert.ElementsMatch(t, []string{"ninja"}, p.FileMetadata.Conflicts)
assert.ElementsMatch(t, []string{"cmake"}, p.FileMetadata.MakeDepends)
assert.ElementsMatch(t, []string{"usr/bin/paket1"}, p.FileMetadata.Backup)
})
}
// TestParsePackageTooManyFiles ensures the accumulated file list is bounded to prevent
// metadata amplification from a package with a huge number of (tiny) file entries.
func TestParsePackageTooManyFiles(t *testing.T) {
defer test.MockVariableValue(&maxFileEntries, 3)()
buf := test.WriteTarCompression(gzip.NewWriter, map[string]string{
"file1": "content1",
".PKGINFO": string(createPKGINFOContent(packageName, packageVersion)),
})
_, err := ParsePackage(buf)
assert.NoError(t, err)
buf = test.WriteTarCompression(gzip.NewWriter, map[string]string{
"file1": "content1",
"file2": "content2",
"file3": "content3",
"file4": "content4",
".PKGINFO": string(createPKGINFOContent(packageName, packageVersion)),
})
_, err = ParsePackage(buf)
assert.ErrorIs(t, err, util.ErrInvalidArgument)
}