mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-11-09 15:49:16 -05:00
- Record the package, version and artifact of a push in one transaction that only inserts the artifact if no pushed copy exists. Of two concurrent pushes of the same file exactly one wins, and the loser's version integrity is rolled back with it. - Check for an existing file before reading the request body, and send Expect: 100-continue from proxy hosted push, so a refused re-push isn't uploaded first. - Leave an existing version record alone for multi-file ecosystems, so a generic push doesn't reset a version cached from upstream.generic. - Reserve the upstream.generic name "hosted" only while hosting is enabled, and only intercept /generic/hosted/ then. - Accept a file argument in proxy hosted delete for generic only. - Mark multi-file ecosystems explicitly, and give parse a min/max range.
220 lines
9 KiB
Go
220 lines
9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type recordedRequest struct {
|
|
method, path, query, authorization, body, expect string
|
|
contentLength int64
|
|
}
|
|
|
|
func hostedTestServer(t *testing.T, status int, response string) (*httptest.Server, *recordedRequest) {
|
|
t.Helper()
|
|
recorded := &recordedRequest{}
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
*recorded = recordedRequest{
|
|
method: r.Method,
|
|
path: r.URL.EscapedPath(),
|
|
query: r.URL.RawQuery,
|
|
authorization: r.Header.Get("Authorization"),
|
|
body: string(body),
|
|
expect: r.Header.Get("Expect"),
|
|
contentLength: r.ContentLength,
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(status)
|
|
_, _ = io.WriteString(w, response)
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
return srv, recorded
|
|
}
|
|
|
|
func hostedEnv(vars map[string]string) func(string) string {
|
|
return func(key string) string { return vars[key] }
|
|
}
|
|
|
|
func writeArchive(t *testing.T, name, content string) string {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), name)
|
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("writing archive: %v", err)
|
|
}
|
|
return path
|
|
}
|
|
|
|
func TestRunPush(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusCreated,
|
|
`{"ecosystem":"npm","name":"@types/node","version":"20.1.0","filename":"node-20.1.0.tgz","sha256":"abc123","size":7}`)
|
|
archive := writeArchive(t, "node-20.1.0.tgz", "tarball")
|
|
var stdout bytes.Buffer
|
|
|
|
err := runPush([]string{"-url", srv.URL, "npm", "@types/node", "20.1.0", archive},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), &stdout, srv.Client())
|
|
if err != nil {
|
|
t.Fatalf("runPush: %v", err)
|
|
}
|
|
if recorded.method != http.MethodPut || recorded.path != "/api/hosted/npm/@types/node/20.1.0" {
|
|
t.Errorf("request = %s %s", recorded.method, recorded.path)
|
|
}
|
|
// sha256 of "tarball", so the proxy can check the upload.
|
|
const tarballSHA256 = "db4b4d0d1cb480bf9aeea253771c00febe627f236765fa37d6a5614f079a3aa0"
|
|
if recorded.authorization != "Bearer tok" || recorded.body != "tarball" || recorded.query != "sha256="+tarballSHA256 {
|
|
t.Errorf("authorization/body/query = %q/%q/%q", recorded.authorization, recorded.body, recorded.query)
|
|
}
|
|
if recorded.expect != "100-continue" || recorded.contentLength != int64(len("tarball")) {
|
|
t.Errorf("expect/content length = %q/%d, want 100-continue and the file size", recorded.expect, recorded.contentLength)
|
|
}
|
|
if out := stdout.String(); !strings.Contains(out, "node-20.1.0.tgz") || !strings.Contains(out, "abc123") {
|
|
t.Errorf("output = %q, want filename and sha256", out)
|
|
}
|
|
}
|
|
|
|
func TestRunPushGenericNamesTheFile(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusCreated,
|
|
`{"ecosystem":"generic","name":"tools","version":"1.2.0","filename":"tool-linux-arm64.tar.gz","sha256":"abc123","size":7}`)
|
|
archive := writeArchive(t, "tool-linux-arm64.tar.gz", "tarball")
|
|
|
|
err := runPush([]string{"-url", srv.URL, "generic", "tools", "1.2.0", archive},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, srv.Client())
|
|
if err != nil {
|
|
t.Fatalf("runPush: %v", err)
|
|
}
|
|
if recorded.path != "/api/hosted/generic/tools/1.2.0/tool-linux-arm64.tar.gz" {
|
|
t.Errorf("path = %q, want the file name appended", recorded.path)
|
|
}
|
|
}
|
|
|
|
func TestRunPushErrors(t *testing.T) {
|
|
srv, _ := hostedTestServer(t, http.StatusBadRequest, `{"code":"BAD_REQUEST","message":"pushing packages is not supported for this ecosystem: cargo"}`)
|
|
archive := writeArchive(t, "serde-1.0.200.crate", "crate")
|
|
|
|
err := runPush([]string{"-url", srv.URL, "cargo", "serde", "1.0.200", archive}, hostedEnv(nil), io.Discard, srv.Client())
|
|
if err == nil || !strings.Contains(err.Error(), "PROXY_HOSTED_TOKEN") {
|
|
t.Errorf("push without token error = %v, want a PROXY_HOSTED_TOKEN hint", err)
|
|
}
|
|
|
|
err = runPush([]string{"-url", srv.URL, "cargo", "serde", "1.0.200", archive},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, srv.Client())
|
|
if err == nil || !strings.Contains(err.Error(), "not supported for this ecosystem") {
|
|
t.Errorf("rejected push error = %v, want the server's message", err)
|
|
}
|
|
|
|
err = runPush([]string{"-url", srv.URL, "npm", "request"}, hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, srv.Client())
|
|
if err == nil || !strings.Contains(err.Error(), "usage") {
|
|
t.Errorf("push with missing arguments error = %v, want usage", err)
|
|
}
|
|
}
|
|
|
|
func TestRunPushUsesProxyURLFromEnv(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusCreated, `{}`)
|
|
archive := writeArchive(t, "request-2.88.3.tgz", "tarball")
|
|
|
|
err := runPush([]string{"npm", "request", "2.88.3", archive},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok", "PROXY_URL": srv.URL}), io.Discard, srv.Client())
|
|
if err != nil {
|
|
t.Fatalf("runPush: %v", err)
|
|
}
|
|
if recorded.path != "/api/hosted/npm/request/2.88.3" {
|
|
t.Errorf("path = %q", recorded.path)
|
|
}
|
|
}
|
|
|
|
func TestRunList(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusOK,
|
|
`{"ecosystem":"npm","name":"request","artifacts":[{"version":"2.88.3","filename":"request-2.88.3.tgz","sha256":"abc123","size":7,"pushed_at":"2026-10-13T10:00:00Z"}]}`)
|
|
var stdout bytes.Buffer
|
|
|
|
if err := runList([]string{"-url", srv.URL, "npm", "request"}, hostedEnv(nil), &stdout, srv.Client()); err != nil {
|
|
t.Fatalf("runList: %v", err)
|
|
}
|
|
if recorded.method != http.MethodGet || recorded.path != "/api/hosted/npm/request" || recorded.authorization != "" {
|
|
t.Errorf("request = %s %s (authorization %q)", recorded.method, recorded.path, recorded.authorization)
|
|
}
|
|
if out := stdout.String(); !strings.Contains(out, "2.88.3") || !strings.Contains(out, "request-2.88.3.tgz") {
|
|
t.Errorf("output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestRunDelete(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusNoContent, "")
|
|
|
|
err := runDelete([]string{"-url", srv.URL, "npm", "request", "2.88.3"},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, srv.Client())
|
|
if err != nil {
|
|
t.Fatalf("runDelete: %v", err)
|
|
}
|
|
if recorded.method != http.MethodDelete || recorded.path != "/api/hosted/npm/request/2.88.3" || recorded.authorization != "Bearer tok" {
|
|
t.Errorf("request = %s %s (authorization %q)", recorded.method, recorded.path, recorded.authorization)
|
|
}
|
|
|
|
missing, _ := hostedTestServer(t, http.StatusNotFound, `{"code":"NOT_FOUND","message":"no pushed artifacts for this version"}`)
|
|
err = runDelete([]string{"-url", missing.URL, "npm", "request", "2.88.3"},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, missing.Client())
|
|
if err == nil || !strings.Contains(err.Error(), "no pushed artifacts") {
|
|
t.Errorf("delete of a missing version error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunDeleteOneFile(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusNoContent, "")
|
|
var stdout bytes.Buffer
|
|
|
|
err := runDelete([]string{"-url", srv.URL, "generic", "tools", "1.2.0", "tool-linux-arm64.tar.gz"},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), &stdout, srv.Client())
|
|
if err != nil {
|
|
t.Fatalf("runDelete: %v", err)
|
|
}
|
|
if recorded.path != "/api/hosted/generic/tools/1.2.0/tool-linux-arm64.tar.gz" {
|
|
t.Errorf("path = %q", recorded.path)
|
|
}
|
|
if out := stdout.String(); out != "deleted generic tools@1.2.0 tool-linux-arm64.tar.gz\n" {
|
|
t.Errorf("output = %q", out)
|
|
}
|
|
}
|
|
|
|
func TestRunDeleteOneFileOnlyForGeneric(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusNoContent, "")
|
|
|
|
err := runDelete([]string{"-url", srv.URL, "npm", "@acme", "widget", "1.0.0"},
|
|
hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, srv.Client())
|
|
if err == nil || !strings.Contains(err.Error(), "only generic files") {
|
|
t.Errorf("npm delete with a fourth argument error = %v, want a refusal", err)
|
|
}
|
|
if recorded.method != "" {
|
|
t.Errorf("refused delete sent %s %s", recorded.method, recorded.path)
|
|
}
|
|
}
|
|
|
|
func TestRunHostedDispatchesSubcommands(t *testing.T) {
|
|
srv, recorded := hostedTestServer(t, http.StatusOK, `{"ecosystem":"npm","name":"request","artifacts":[]}`)
|
|
|
|
if err := runHosted([]string{"list", "-url", srv.URL, "npm", "request"}, hostedEnv(nil), io.Discard, srv.Client()); err != nil {
|
|
t.Fatalf("runHosted list: %v", err)
|
|
}
|
|
if recorded.method != http.MethodGet || recorded.path != "/api/hosted/npm/request" {
|
|
t.Errorf("request = %s %s", recorded.method, recorded.path)
|
|
}
|
|
|
|
for _, args := range [][]string{nil, {"bogus"}} {
|
|
err := runHosted(args, hostedEnv(nil), io.Discard, srv.Client())
|
|
if err == nil || !strings.Contains(err.Error(), "usage: proxy hosted <push|list|delete>") {
|
|
t.Errorf("runHosted(%q) error = %v, want the hosted usage", args, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestHostedSubcommandUsageNamesTheHostedCommand(t *testing.T) {
|
|
err := runPush([]string{"npm"}, hostedEnv(map[string]string{"PROXY_HOSTED_TOKEN": "tok"}), io.Discard, http.DefaultClient)
|
|
if err == nil || !strings.Contains(err.Error(), "usage: proxy hosted push") {
|
|
t.Errorf("push usage error = %v, want it to name proxy hosted push", err)
|
|
}
|
|
}
|