4c321a089d
- serve sparse index and config.json for local cargo repos from a cargo_index table - add Web API publish, yank and unyank under the local repo api root - drop index lines when the crate file is deleted
219 lines
7.1 KiB
Go
219 lines
7.1 KiB
Go
package cargo
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.unkin.net/unkin/artifactapi/internal/provider"
|
|
)
|
|
|
|
func publishBody(meta string, crate []byte) []byte {
|
|
var b []byte
|
|
b = binary.LittleEndian.AppendUint32(b, uint32(len(meta)))
|
|
b = append(b, meta...)
|
|
b = binary.LittleEndian.AppendUint32(b, uint32(len(crate)))
|
|
return append(b, crate...)
|
|
}
|
|
|
|
const sampleMeta = `{
|
|
"name": "unkin-hello", "vers": "0.2.0",
|
|
"deps": [
|
|
{"name": "serde", "version_req": "^1", "features": ["derive"], "optional": false,
|
|
"default_features": true, "target": null, "kind": "normal",
|
|
"registry": "https://github.com/rust-lang/crates.io-index"},
|
|
{"name": "rand_core", "version_req": "^0.6", "features": null, "optional": true,
|
|
"default_features": false, "target": "cfg(unix)", "kind": "dev",
|
|
"explicit_name_in_toml": "rc"}
|
|
],
|
|
"features": {"default": ["std"], "std": [], "extra": ["dep:rc"], "weak": ["serde?/std"]},
|
|
"links": null, "rust_version": "1.70", "description": "ignored"
|
|
}`
|
|
|
|
func TestParsePublishAndIndexLine(t *testing.T) {
|
|
crate := []byte("fake crate bytes")
|
|
meta, gotCrate, err := ParsePublish(publishBody(sampleMeta, crate))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(gotCrate) != string(crate) {
|
|
t.Fatalf("crate = %q", gotCrate)
|
|
}
|
|
line, err := NewIndexLine(meta, crate)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
sum := sha256.Sum256(crate)
|
|
if line.Cksum != hex.EncodeToString(sum[:]) {
|
|
t.Errorf("cksum = %s", line.Cksum)
|
|
}
|
|
|
|
raw, _ := json.Marshal(line)
|
|
var got map[string]any
|
|
_ = json.Unmarshal(raw, &got)
|
|
if _, ok := got["version_req"]; ok {
|
|
t.Error("publish-only field leaked into index line")
|
|
}
|
|
if got["v"] != float64(2) {
|
|
t.Errorf("v = %v, want 2 with features2 present", got["v"])
|
|
}
|
|
if got["rust_version"] != "1.70" {
|
|
t.Errorf("rust_version = %v", got["rust_version"])
|
|
}
|
|
if _, ok := line.Features["extra"]; ok {
|
|
t.Error("dep: feature must move to features2")
|
|
}
|
|
if len(line.Features2) != 2 || len(line.Features) != 2 {
|
|
t.Errorf("features = %v features2 = %v", line.Features, line.Features2)
|
|
}
|
|
|
|
deps := got["deps"].([]any)
|
|
serde := deps[0].(map[string]any)
|
|
if serde["req"] != "^1" || serde["registry"] != "https://github.com/rust-lang/crates.io-index" || serde["target"] != nil {
|
|
t.Errorf("serde dep = %v", serde)
|
|
}
|
|
if _, ok := serde["package"]; ok {
|
|
t.Error("package set without a rename")
|
|
}
|
|
renamed := deps[1].(map[string]any)
|
|
if renamed["name"] != "rc" || renamed["package"] != "rand_core" || renamed["kind"] != "dev" {
|
|
t.Errorf("renamed dep = %v", renamed)
|
|
}
|
|
if f, ok := renamed["features"].([]any); !ok || len(f) != 0 {
|
|
t.Errorf("null features must render as [], got %v", renamed["features"])
|
|
}
|
|
}
|
|
|
|
func TestIndexLineNoNewFeaturesOmitsV(t *testing.T) {
|
|
line, err := NewIndexLine(PublishMeta{Name: "a", Vers: "1.0.0", Features: map[string][]string{"x": nil}}, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
raw, _ := json.Marshal(line)
|
|
s := string(raw)
|
|
for _, bad := range []string{`"v":`, `"features2"`, `"links"`} {
|
|
if strings.Contains(s, bad) {
|
|
t.Errorf("%s present in %s", bad, s)
|
|
}
|
|
}
|
|
if !strings.Contains(s, `"deps":[]`) || !strings.Contains(s, `"features":{"x":[]}`) {
|
|
t.Errorf("deps/features must be non-null: %s", s)
|
|
}
|
|
}
|
|
|
|
func TestParsePublishRejectsMalformed(t *testing.T) {
|
|
good := publishBody(`{"name":"a","vers":"1.0.0"}`, []byte("c"))
|
|
for name, body := range map[string][]byte{
|
|
"empty": nil,
|
|
"short": good[:len(good)-1],
|
|
"trailing": append(append([]byte{}, good...), 'x'),
|
|
"bad json": publishBody(`{`, []byte("c")),
|
|
"no length": good[:6],
|
|
} {
|
|
if _, _, err := ParsePublish(body); err == nil {
|
|
t.Errorf("%s: expected error", name)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestNewIndexLineValidation(t *testing.T) {
|
|
for _, m := range []PublishMeta{
|
|
{Name: "", Vers: "1.0.0"},
|
|
{Name: "1abc", Vers: "1.0.0"},
|
|
{Name: "../x", Vers: "1.0.0"},
|
|
{Name: "ok", Vers: "1.0.0/../../x"},
|
|
{Name: "ok", Vers: ""},
|
|
{Name: strings.Repeat("a", 65), Vers: "1.0.0"},
|
|
} {
|
|
if _, err := NewIndexLine(m, nil); err == nil {
|
|
t.Errorf("%q@%q accepted", m.Name, m.Vers)
|
|
}
|
|
}
|
|
if _, err := NewIndexLine(PublishMeta{Name: "Foo_bar-2", Vers: "1.0.0-rc.1+build.5"}, nil); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
|
|
func TestIndexPath(t *testing.T) {
|
|
for name, want := range map[string]string{
|
|
"a": "1/a", "ab": "2/ab", "abc": "3/a/abc", "Serde": "se/rd/serde", "unkin-hello": "un/ki/unkin-hello",
|
|
} {
|
|
if got := IndexPath(name); got != want {
|
|
t.Errorf("IndexPath(%q) = %q, want %q", name, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
type fakeIndex struct {
|
|
entries []provider.CargoIndexEntry
|
|
asked string
|
|
}
|
|
|
|
func (f *fakeIndex) ListCargoIndexEntries(_ context.Context, _, name string) ([]provider.CargoIndexEntry, error) {
|
|
f.asked = name
|
|
return f.entries, nil
|
|
}
|
|
func (f *fakeIndex) ListFilesByPrefix(context.Context, string, string) ([]provider.FileEntry, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeIndex) ListPackages(context.Context, string) ([]string, error) { return nil, nil }
|
|
|
|
func TestServeLocalIndex(t *testing.T) {
|
|
line, _ := NewIndexLine(PublishMeta{Name: "unkin-hello", Vers: "0.1.0"}, []byte("x"))
|
|
raw, _ := json.Marshal(line)
|
|
files := &fakeIndex{entries: []provider.CargoIndexEntry{
|
|
{Name: "unkin-hello", Version: "0.1.0", Line: string(raw), Yanked: true},
|
|
{Name: "unkin-hello", Version: "0.2.0", Line: strings.Replace(string(raw), "0.1.0", "0.2.0", 1)},
|
|
}}
|
|
p := &Provider{}
|
|
|
|
rec := httptest.NewRecorder()
|
|
if !p.ServeLocalIndex(rec, httptest.NewRequest("GET", "http://aa.example/x", nil), files, "crates-local", "un/ki/unkin-hello") {
|
|
t.Fatal("index path not served")
|
|
}
|
|
lines := strings.Split(strings.TrimSpace(rec.Body.String()), "\n")
|
|
if len(lines) != 2 || !strings.Contains(lines[0], `"yanked":true`) || !strings.Contains(lines[1], `"yanked":false`) {
|
|
t.Errorf("index body = %q", rec.Body.String())
|
|
}
|
|
if files.asked != "unkin-hello" {
|
|
t.Errorf("looked up %q", files.asked)
|
|
}
|
|
|
|
rec = httptest.NewRecorder()
|
|
req := httptest.NewRequest("GET", "http://aa.example/config.json", nil)
|
|
req.Header.Set("X-Forwarded-Proto", "https")
|
|
p.ServeLocalIndex(rec, req, files, "crates-local", "config.json")
|
|
var cfg map[string]string
|
|
_ = json.Unmarshal(rec.Body.Bytes(), &cfg)
|
|
if cfg["api"] != "https://aa.example/api/v1/local/crates-local" ||
|
|
cfg["dl"] != "https://aa.example/api/v1/local/crates-local/crates/{crate}/{crate}-{version}.crate" {
|
|
t.Errorf("config = %v", cfg)
|
|
}
|
|
|
|
for _, path := range []string{"crates/unkin-hello/unkin-hello-0.1.0.crate", "xx/ki/unkin-hello", ""} {
|
|
if p.ServeLocalIndex(httptest.NewRecorder(), httptest.NewRequest("GET", "/", nil), files, "r", path) {
|
|
t.Errorf("%q should fall through", path)
|
|
}
|
|
}
|
|
|
|
files.entries = nil
|
|
rec = httptest.NewRecorder()
|
|
p.ServeLocalIndex(rec, httptest.NewRequest("GET", "/", nil), files, "r", "3/a/abc")
|
|
if rec.Code != 404 {
|
|
t.Errorf("unknown crate code = %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestCratePathMatchesDL(t *testing.T) {
|
|
dl := LocalConfig("http://h/api/v1/local/r")["dl"]
|
|
want := strings.NewReplacer("{crate}", "unkin-hello", "{version}", "0.1.0").Replace(dl)
|
|
if got := "http://h/api/v1/local/r/" + CratePath("unkin-hello", "0.1.0"); got != want {
|
|
t.Errorf("CratePath = %q, dl expands to %q", got, want)
|
|
}
|
|
}
|