Files
gitea/modules/packages/npm/creator_test.go
T
silverwind 51b93d1d27 enhance(packages/npm): improve npm client compatibility (#39434)
Aligns the npm registry with what npm, pnpm and yarn expect:

1. Raise the publish body cap from
https://github.com/go-gitea/gitea/pull/37890 to 256 MiB like npmjs,
larger bodies get 413
2. Pick the tarball attachment by name, `npm publish --provenance`
failed at random
3. Store and serve `libc`, so mismatched glibc/musl optional binaries
are skipped
4. Treat root `*.gyp` files as an install script, like npm does
5. Always serve a `latest` dist-tag, yarn and pnpm fail without it
6. Take top-level metadata from `latest` and drop the per-version readme
7. Serve tarballs at the npmjs path `/<name>/-/<file>`, former URLs keep
working
8. Add ETag revalidation for metadata, `npm ping` and `npm whoami`

Tested with npm 12.1, pnpm 12.4, yarn 1.22 and yarn 4.18.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
2026-10-01 07:50:47 +00:00

474 lines
12 KiB
Go

// Copyright 2021 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
package npm
import (
"bytes"
"compress/gzip"
"crypto/sha512"
"encoding/base64"
"fmt"
"strings"
"testing"
"gitea.dev/modules/json"
"gitea.dev/modules/test"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestParsePackage(t *testing.T) {
packageScope := "@scope"
packageName := "test-package"
packageFullName := packageScope + "/" + packageName
packageVersion := "1.0.1-pre"
packageTag := "latest"
packageAuthor := "KN4CK3R"
packageBin := "gitea"
packageDescription := "Test Description"
repository := Repository{
Type: "gitea",
URL: "http://localhost:3000/gitea/test.git",
Directory: "packages/test-package",
}
dataBytes := buildTarball(map[string]string{
"package/package.json": `{"name": "@scope/test-package","version": "1.0.1-pre","description": "Test Description","author": "KN4CK3R"}`,
})
data := base64.StdEncoding.EncodeToString(dataBytes)
integrity := "sha512-" + base64Sha512(dataBytes)
t.Run("InvalidUpload", func(t *testing.T) {
p, _, err := ParseUpload(bytes.NewReader([]byte{0}))
assert.Nil(t, p)
assert.Error(t, err)
})
t.Run("InvalidUploadNoData", func(t *testing.T) {
p, err := parseUploadPackage(&packageUpload{})
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackage)
})
t.Run("InvalidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) {
p, err := parseUploadPackage(&packageUpload{
PackageMetadata: PackageMetadata{
ID: name,
Name: name,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: name,
},
},
},
})
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageName)
}
test(t, " test ")
test(t, " test")
test(t, "test ")
test(t, "te st")
test(t, "Test")
test(t, "_test")
test(t, ".test")
test(t, "^test")
test(t, "te^st")
test(t, "te|st")
test(t, "te)(st")
test(t, "te'st")
test(t, "te!st")
test(t, "te*st")
test(t, "te~st")
test(t, "invalid/scope")
test(t, "@invalid/_name")
test(t, "@invalid/.name")
})
t.Run("ValidPackageName", func(t *testing.T) {
test := func(t *testing.T, name string) {
p, err := parseUploadPackage(&packageUpload{
PackageMetadata: PackageMetadata{
ID: name,
Name: name,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: name,
},
},
},
})
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
}
test(t, "test")
test(t, "@scope/name")
test(t, "@scope/q")
test(t, "q")
test(t, "@scope/package-name")
test(t, "@scope/package.name")
test(t, "@scope/package_name")
test(t, "123name")
test(t, "----")
test(t, packageFullName)
})
t.Run("InvalidPackageVersion", func(t *testing.T) {
version := "first-version"
p, err := parseUploadPackage(&packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
version: {
Name: packageFullName,
Version: version,
},
},
},
})
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidPackageVersion)
})
t.Run("InvalidAttachment", func(t *testing.T) {
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
},
},
},
Attachments: map[string]*PackageAttachment{
"dummy.tgz": {},
},
})
p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidAttachment)
})
t.Run("InvalidData", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: "/",
},
},
})
p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidAttachment)
})
t.Run("InvalidIntegrity", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
Dist: PackageDistribution{
Integrity: "sha512-test==",
},
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: data,
},
},
})
p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidIntegrity)
})
t.Run("InvalidIntegrity2", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
Dist: PackageDistribution{
Integrity: integrity,
},
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: base64.StdEncoding.EncodeToString([]byte("data")),
},
},
})
p, _, err := ParseUpload(bytes.NewReader(b))
assert.Nil(t, p)
assert.ErrorIs(t, err, ErrInvalidIntegrity)
})
t.Run("Valid", func(t *testing.T) {
filename := fmt.Sprintf("%s-%s.tgz", packageFullName, packageVersion)
b, _ := json.Marshal(packageUpload{
PackageMetadata: PackageMetadata{
ID: packageFullName,
Name: packageFullName,
DistTags: map[string]string{
packageTag: packageVersion,
},
Versions: map[string]*PackageMetadataVersion{
packageVersion: {
Name: packageFullName,
Version: packageVersion,
Description: packageDescription,
Author: User{Name: packageAuthor},
License: "MIT",
Homepage: "https://gitea.io/",
Readme: packageDescription,
Dependencies: map[string]string{
"package": "1.2.0",
},
Bin: map[string]string{
"bin": packageBin,
},
Dist: PackageDistribution{
Integrity: integrity,
},
Repository: repository,
},
},
},
Attachments: map[string]*PackageAttachment{
filename: {
Data: data,
},
packageFullName + "-" + packageVersion + ".sigstore": {
Data: "{}",
},
},
})
p, _, err := ParseUpload(bytes.NewReader(b))
assert.NotNil(t, p)
assert.NoError(t, err)
assert.Equal(t, packageFullName, p.Name)
assert.Equal(t, packageVersion, p.Version)
assert.Equal(t, []string{packageTag}, p.DistTags)
assert.Equal(t, fmt.Sprintf("%s-%s.tgz", strings.Split(packageFullName, "/")[1], packageVersion), p.Filename)
b, _ = base64.StdEncoding.DecodeString(data)
assert.Equal(t, b, p.Data)
assert.Equal(t, packageName, p.Metadata.Name)
assert.Equal(t, packageScope, p.Metadata.Scope)
assert.Equal(t, packageDescription, p.Metadata.Description)
assert.Equal(t, packageDescription, p.Metadata.Readme)
assert.Equal(t, packageAuthor, p.Metadata.Author)
assert.Equal(t, packageBin, p.Metadata.Bin["bin"])
assert.Equal(t, "MIT", string(p.Metadata.License))
assert.Equal(t, "https://gitea.io/", p.Metadata.ProjectURL)
assert.Contains(t, p.Metadata.Dependencies, "package")
assert.Equal(t, "1.2.0", p.Metadata.Dependencies["package"])
assert.Equal(t, repository.Type, p.Metadata.Repository.Type)
assert.Equal(t, repository.URL, p.Metadata.Repository.URL)
assert.Equal(t, repository.Directory, p.Metadata.Repository.Directory)
})
t.Run("ValidLicenseMap", func(t *testing.T) {
packageJSON := `{
"versions": {
"0.1.1": {
"name": "dev-null",
"version": "0.1.1",
"license": {
"type": "MIT"
},
"dist": {
"integrity": "sha256-"
}
}
},
"_attachments": {
"foo": {
"data": "AAAA"
}
}
}`
p, _, err := ParseUpload(strings.NewReader(packageJSON))
require.NoError(t, err)
require.Equal(t, "MIT", string(p.Metadata.License))
})
t.Run("ValidRepositoryAndBinAsString", func(t *testing.T) {
// npm allows "repository" and "bin" to be plain strings, not only objects.
packageJSON := `{
"versions": {
"0.1.1": {
"name": "dev-null",
"version": "0.1.1",
"bin": "./cli.js",
"repository": "https://gitea.io/gitea/test.git",
"dist": {
"integrity": "sha256-"
}
}
},
"_attachments": {
"foo": {
"data": "AAAA"
}
}
}`
p, _, err := ParseUpload(strings.NewReader(packageJSON))
require.NoError(t, err)
require.Equal(t, "https://gitea.io/gitea/test.git", p.Metadata.Repository.URL)
// a string bin is named after the package
require.Equal(t, "./cli.js", p.Metadata.Bin["dev-null"])
})
}
// buildTarball assembles a gzipped tar with the given entries.
func buildTarball(files map[string]string) []byte {
return test.WriteTarCompression(gzip.NewWriter, files).Bytes()
}
func TestInspectTarball(t *testing.T) {
cases := []struct {
name string
files map[string]string
wantShrinkwrap, wantInstaller bool
}{
{
name: "empty",
files: map[string]string{},
},
{
name: "shrinkwrap only",
files: map[string]string{"package/npm-shrinkwrap.json": "{}"},
wantShrinkwrap: true,
},
{
name: "postinstall only",
files: map[string]string{"package/package.json": `{"scripts":{"postinstall":"echo hi"}}`},
wantInstaller: true,
},
{
name: "preinstall",
files: map[string]string{"package/package.json": `{"scripts":{"preinstall":"noop"}}`},
wantInstaller: true,
},
{
name: "install",
files: map[string]string{"package/package.json": `{"scripts":{"install":"noop"}}`},
wantInstaller: true,
},
{
name: "whitespace-only script does not count",
files: map[string]string{"package/package.json": `{"scripts":{"postinstall":" "}}`},
},
{
name: "unrelated lifecycle script ignored",
files: map[string]string{"package/package.json": `{"scripts":{"test":"jest"}}`},
},
{
name: "both",
files: map[string]string{
"package/npm-shrinkwrap.json": "{}",
"package/package.json": `{"scripts":{"install":"go"}}`,
},
wantShrinkwrap: true,
wantInstaller: true,
},
{
name: "nested shrinkwrap ignored",
files: map[string]string{
"package/subdir/npm-shrinkwrap.json": "{}",
},
},
{
name: "leading ./ prefix stripped",
files: map[string]string{"./package/npm-shrinkwrap.json": "{}"},
// npm pack sometimes emits "./package/..." entries.
wantShrinkwrap: true,
},
{
name: "gyp file implies node-gyp install",
files: map[string]string{"package/binding.gyp": "{}"},
wantInstaller: true,
},
{
name: "gypfile false disables gyp install",
files: map[string]string{"package/binding.gyp": "{}", "package/package.json": `{"gypfile":false}`},
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
data := buildTarball(c.files)
gotShrink, gotInstaller := inspectTarball(data)
assert.Equal(t, c.wantShrinkwrap, gotShrink, "shrinkwrap")
assert.Equal(t, c.wantInstaller, gotInstaller, "installScript")
})
}
t.Run("malformed gzip returns false", func(t *testing.T) {
hasShrinkwrap, hasInstaller := inspectTarball([]byte("not a gzip"))
assert.False(t, hasShrinkwrap)
assert.False(t, hasInstaller)
})
t.Run("malformed package.json falls through", func(t *testing.T) {
data := buildTarball(map[string]string{"package/package.json": "{ this is not json"})
_, hasInstaller := inspectTarball(data)
assert.False(t, hasInstaller)
})
}
func TestParseUpload(t *testing.T) {
pkg := "@scope/test-package"
t.Run("dispatches deprecate on missing _attachments", func(t *testing.T) {
body := fmt.Sprintf(`{"name":%q,"versions":{"1.0.0":{"deprecated":"gone"},"1.0.1":{"deprecated":""},"1.0.2":{},"1.0.3":null}}`, pkg)
p, dep, err := ParseUpload(strings.NewReader(body))
require.NoError(t, err)
assert.Nil(t, p)
require.NotNil(t, dep)
assert.Equal(t, map[string]string{"1.0.0": "gone", "1.0.1": ""}, dep.Versions)
})
}
func base64Sha512(data []byte) string {
h := sha512.Sum512(data)
return base64.StdEncoding.EncodeToString(h[:])
}