Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 442c7870eb |
@@ -4,6 +4,3 @@ bin/
|
||||
# e2e-docker fixtures are real package files (.rpm, .tgz, .whl, .zip, ...) that
|
||||
# are intentionally tracked, overriding any global ignore of those extensions.
|
||||
!e2e-docker/fixtures/**
|
||||
|
||||
# Written per run by the dockerised e2e TestMain.
|
||||
e2e-docker/oidc/jwks.json
|
||||
|
||||
@@ -21,17 +21,7 @@ services:
|
||||
volumes:
|
||||
- ./e2e-docker/mirror-conf/b.conf:/etc/nginx/conf.d/default.conf:ro,z
|
||||
|
||||
# Static OIDC issuer gating writes; the suite's TestMain writes jwks.json.
|
||||
mockoidc:
|
||||
image: nginx:alpine
|
||||
volumes:
|
||||
- ./e2e-docker/oidc:/usr/share/nginx/html:ro,z
|
||||
|
||||
artifactapi:
|
||||
environment:
|
||||
ARTIFACTAPI_OIDC_ISSUER: http://mockoidc
|
||||
ARTIFACTAPI_OIDC_AUDIENCE: artifactapi
|
||||
ARTIFACTAPI_AUTH_UPLOADS: "true"
|
||||
# The host port is set via ARTIFACTAPI_PORT (see scripts/docker-e2e.sh),
|
||||
# defaulting to 8000; the e2e run uses 8001 to avoid colliding with a
|
||||
# locally-running instance.
|
||||
@@ -42,5 +32,3 @@ services:
|
||||
condition: service_started
|
||||
mockupstreamb:
|
||||
condition: service_started
|
||||
mockoidc:
|
||||
condition: service_started
|
||||
|
||||
@@ -21,13 +21,6 @@ already-running stack.
|
||||
|
||||
## Coverage
|
||||
|
||||
- **Write auth** — the stack runs with `ARTIFACTAPI_AUTH_UPLOADS=true` against a
|
||||
static `mockoidc` issuer (`oidc/`); `TestMain` generates the signing key, writes
|
||||
`oidc/jwks.json` and mints the tokens. Every local publish authenticates the way
|
||||
its real client does (curl Bearer/Basic, `docker login`, npm `_authToken`,
|
||||
`GEM_HOST_API_KEY`, `ansible-galaxy --api-key`), and each type checks a
|
||||
publish without a token or with a foreign-key token is refused while
|
||||
anonymous reads still work.
|
||||
- **Repository lifecycle** — add / change / delete for remote, local and virtual repos.
|
||||
- **Caching** — one immutable artifact per remote package type (one
|
||||
`caching_<type>_test.go` case each) proxied through the mock upstream: first fetch `X-Artifact-Source: remote`, second `cache`, bytes
|
||||
|
||||
@@ -47,20 +47,18 @@ func TestLocalAnsiblePublishInstall(t *testing.T) {
|
||||
defer deleteRepo(t, name)
|
||||
|
||||
script := "set -euo pipefail\n" + ansibleHelloScript + `
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key "$TOKEN"
|
||||
if ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key "$TOKEN"; then echo "republish accepted"; exit 1; fi
|
||||
cd /w/unkin/e2e_hello && sed -i 's/^version: .*/version: 1.0.1/' galaxy.yml && ansible-galaxy collection build --output-path /w/dist >/dev/null
|
||||
if ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.1.tar.gz --server "$SRV" --api-key "$BAD"; then echo "invalid key publish accepted"; exit 1; fi
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key unused
|
||||
if ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$SRV" --api-key unused; then echo "republish accepted"; exit 1; fi
|
||||
ansible-galaxy collection install unkin.e2e_hello:==1.0.0 --server "$SRV" -p /w/coll
|
||||
` + ansibleHelloPlay
|
||||
|
||||
srv := strings.TrimRight(internal, "/") + "/api/v1/local/" + name + "/"
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "SRV="+srv, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken, "python:3.12-slim", "bash", "-c", script).CombinedOutput()
|
||||
"-e", "SRV="+srv, "python:3.12-slim", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("ansible-galaxy client run failed: %v\n%s", err, out)
|
||||
}
|
||||
for _, want := range []string{"successfully published and imported", "HTTP Code: 409", "HTTP Code: 401", "unkin.e2e_hello:1.0.0 was installed successfully", "PLAY hello e2e"} {
|
||||
for _, want := range []string{"successfully published and imported", "HTTP Code: 409", "unkin.e2e_hello:1.0.0 was installed successfully", "PLAY hello e2e"} {
|
||||
if !strings.Contains(string(out), want) {
|
||||
t.Fatalf("missing %q in client output:\n%s", want, out)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestVirtualAnsibleInstall(t *testing.T) {
|
||||
|
||||
base := strings.TrimRight(internal, "/")
|
||||
script := "set -euo pipefail\n" + ansibleHelloScript + `
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$BASE/api/v1/local/$B/" --api-key "$TOKEN"
|
||||
ansible-galaxy collection publish /w/dist/unkin-e2e_hello-1.0.0.tar.gz --server "$BASE/api/v1/local/$B/" --api-key unused
|
||||
V="$BASE/api/v1/virtual/$VIRT"
|
||||
ansible-galaxy collection install unkin.e2e_hello --server "$V/" -p /w/coll
|
||||
DL=$(python3 -c 'import json,sys,urllib.request as u;print(json.load(u.urlopen(sys.argv[1]))["download_url"])' "$V/api/v3/collections/unkin/e2e_hello/versions/1.0.0/")
|
||||
@@ -47,7 +47,7 @@ except u.HTTPError as e: print(e.code, e.headers["Location"])' "$DL")"
|
||||
` + ansibleHelloPlay
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "BASE="+base, "-e", "B="+b, "-e", "VIRT="+virt, "-e", "TOKEN="+authToken,
|
||||
"-e", "BASE="+base, "-e", "B="+b, "-e", "VIRT="+virt,
|
||||
"python:3.12-slim", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("ansible-galaxy client run failed: %v\n%s", err, out)
|
||||
|
||||
@@ -8,8 +8,6 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -81,12 +79,8 @@ func TestLocalDockerPushPull(t *testing.T) {
|
||||
const image = "team/app"
|
||||
const tag = "v1.0.0"
|
||||
|
||||
// /v2/ version check: anonymous gets the token challenge, a token passes.
|
||||
resp, _ := doRequestAuth(t, http.MethodGet, api("/v2/"), nil, "", "")
|
||||
if resp.StatusCode != http.StatusUnauthorized || !strings.Contains(resp.Header.Get("WWW-Authenticate"), "/v2/token") {
|
||||
t.Fatalf("anonymous /v2/ ping: status %d, challenge %q", resp.StatusCode, resp.Header.Get("WWW-Authenticate"))
|
||||
}
|
||||
resp, _ = doRequest(t, http.MethodGet, api("/v2/"), nil, "")
|
||||
// /v2/ version check.
|
||||
resp, _ := doRequest(t, http.MethodGet, api("/v2/"), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("/v2/ ping: status %d", resp.StatusCode)
|
||||
}
|
||||
@@ -175,66 +169,9 @@ func TestLocalDockerPushPull(t *testing.T) {
|
||||
t.Fatalf("tags/list wrong repository name: %s", body)
|
||||
}
|
||||
|
||||
assertWriteGated(t, http.MethodPut, api("/v2/docker-internal/"+image+"/manifests/v2"), manifest, manifestType, api("/v2/docker-internal/"+image+"/manifests/"+tag))
|
||||
|
||||
// A now-present blob HEAD should succeed (client would skip re-upload).
|
||||
resp, _ = doRequest(t, http.MethodHead, api("/v2/docker-internal/"+image+"/blobs/"+layerDigest), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("post-push blob HEAD: expected 200, got %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalDockerCLIPushPull drives the host docker CLI: docker login with the
|
||||
// token as password then push, a push without login rejected, an anonymous
|
||||
// pull, and a buildx (buildkit) push.
|
||||
func TestLocalDockerCLIPushPull(t *testing.T) {
|
||||
if _, err := exec.LookPath("docker"); err != nil {
|
||||
t.Skip("docker not available on the test host")
|
||||
}
|
||||
createRepo(t, `{"name":"docker-cli","package_type":"docker","repo_type":"local"}`)
|
||||
defer deleteRepo(t, "docker-cli")
|
||||
|
||||
reg := strings.TrimPrefix(baseURL(), "http://")
|
||||
img := reg + "/docker-cli/e2e/hello"
|
||||
cfg := t.TempDir()
|
||||
docker := func(stdin []byte, args ...string) (string, error) {
|
||||
cmd := exec.Command("docker", args...)
|
||||
cmd.Env = append(os.Environ(), "DOCKER_CONFIG="+cfg)
|
||||
cmd.Stdin = bytes.NewReader(stdin)
|
||||
out, err := cmd.CombinedOutput()
|
||||
return string(out), err
|
||||
}
|
||||
must := func(t *testing.T, stdin []byte, args ...string) {
|
||||
t.Helper()
|
||||
if out, err := docker(stdin, args...); err != nil {
|
||||
t.Fatalf("docker %s: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
}
|
||||
defer func() { _, _ = docker(nil, "rmi", "-f", img+":v1", img+":anon", img+":buildx") }()
|
||||
|
||||
must(t, make([]byte, 1024), "import", "-", img+":v1") // an empty tar
|
||||
|
||||
must(t, []byte(authToken), "login", reg, "-u", "ci", "--password-stdin")
|
||||
must(t, nil, "push", img+":v1")
|
||||
|
||||
must(t, nil, "logout", reg)
|
||||
must(t, nil, "tag", img+":v1", img+":anon")
|
||||
if out, err := docker(nil, "push", img+":anon"); err == nil {
|
||||
t.Fatalf("push without login accepted:\n%s", out)
|
||||
}
|
||||
must(t, nil, "rmi", img+":v1", img+":anon")
|
||||
must(t, nil, "pull", img+":v1")
|
||||
|
||||
t.Run("buildx", func(t *testing.T) {
|
||||
t.Skip("buildkit fetches the registry token with an OAuth2 POST form the server ignores; fixed by unkin/artifactapi#180")
|
||||
conf := cfg + "/buildkitd.toml"
|
||||
if err := os.WriteFile(conf, []byte(fmt.Sprintf("[registry.%q]\n http = true\n", reg)), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
builder := fmt.Sprintf("e2e-%d", os.Getpid())
|
||||
must(t, nil, "buildx", "create", "--name", builder, "--driver", "docker-container", "--driver-opt", "network=host", "--buildkitd-config", conf)
|
||||
defer func() { _, _ = docker(nil, "buildx", "rm", "-f", builder) }()
|
||||
must(t, []byte(authToken), "login", reg, "-u", "ci", "--password-stdin")
|
||||
must(t, []byte("FROM "+img+":v1\nLABEL e2e=buildx\n"), "buildx", "build", "--builder", builder, "--push", "-t", img+":buildx", "-")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestLocalGemsPushInstall(t *testing.T) {
|
||||
|
||||
src := strings.TrimRight(internal, "/") + "/api/v1/local/" + name
|
||||
script := `set -euo pipefail
|
||||
export GEM_HOST_API_KEY="$TOKEN"
|
||||
export GEM_HOST_API_KEY=unused
|
||||
mk() { d=/w/src-$1${2:+-b}; mkdir -p $d/lib; cd $d
|
||||
echo "module UnkinE2eHello; VERSION = '$1'; end${2:-}" > lib/unkin-e2e-hello.rb
|
||||
printf 'Gem::Specification.new { |s| s.name = "unkin-e2e-hello"; s.version = "%s"; s.summary = "e2e"; s.authors = ["e2e"]; s.files = ["lib/unkin-e2e-hello.rb"] }' "$1" > g.gemspec
|
||||
@@ -52,15 +52,14 @@ echo "INFO $(curl -s "$SRC/info/unkin-e2e-hello" | cut -d' ' -f1 | tr '\n' ' ')"
|
||||
mk 0.2.0 '# changed' && if gem push --host "$SRC" unkin-e2e-hello-0.2.0.gem; then echo "yanked re-push accepted"; exit 1; fi
|
||||
cd /w/app && gem uninstall -a -x unkin-e2e-hello >/dev/null
|
||||
if gem install --remote --no-document unkin-e2e-hello -v 0.2.0; then echo "yanked version installed"; exit 1; fi
|
||||
mk 0.3.0 && if GEM_HOST_API_KEY="$BAD" gem push --host "$SRC" unkin-e2e-hello-0.3.0.gem; then echo "invalid key push accepted"; exit 1; fi
|
||||
echo "VERSIONS $(curl -s "$SRC/versions" | grep '^unkin-e2e-hello ' | cut -d' ' -f2)"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "SRC="+src, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken, "ruby:3.3", "bash", "-c", script).CombinedOutput()
|
||||
"-e", "SRC="+src, "ruby:3.3", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("gem client run failed: %v\n%s", err, out)
|
||||
}
|
||||
for _, want := range []string{"GEM 0.2.0", "BUNDLE 0.2.0", "INFO --- 0.1.0 ", "re-pushing a yanked version is not allowed", `{"error":"invalid token"}`, "VERSIONS 0.1.0\n"} {
|
||||
for _, want := range []string{"GEM 0.2.0", "BUNDLE 0.2.0", "INFO --- 0.1.0 ", "re-pushing a yanked version is not allowed", "VERSIONS 0.1.0\n"} {
|
||||
if !strings.Contains(string(out), want) {
|
||||
t.Fatalf("missing %q in gem output:\n%s", want, out)
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ func TestVirtualGemsInstall(t *testing.T) {
|
||||
|
||||
base := strings.TrimRight(internal, "/")
|
||||
script := `set -euo pipefail
|
||||
export GEM_HOST_API_KEY="$TOKEN"
|
||||
export GEM_HOST_API_KEY=unused
|
||||
pub() { d=/w/src-$1-$2; mkdir -p $d/lib; cd $d
|
||||
echo "module UnkinE2eHello; VERSION = '$1:$2'; end" > lib/unkin-e2e-hello.rb
|
||||
printf 'Gem::Specification.new { |s| s.name = "unkin-e2e-hello"; s.version = "%s"; s.summary = "e2e"; s.authors = ["e2e"]; s.files = ["lib/unkin-e2e-hello.rb"] }' "$2" > g.gemspec
|
||||
@@ -52,7 +52,7 @@ bundle exec ruby -e 'require "unkin-e2e-hello"; puts "REQ #{UnkinE2eHello::VERSI
|
||||
echo "REDIRECT $(curl -s -o /dev/null -w '%{redirect_url}' "$V/gems/unkin-e2e-hello-0.2.0.gem")"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt, "-e", "TOKEN="+authToken,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt,
|
||||
"ruby:3.3", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("bundler run failed: %v\n%s", err, out)
|
||||
|
||||
@@ -37,8 +37,6 @@ func TestLocalGoProxy(t *testing.T) {
|
||||
uploadFile(t, "local-go", mod+"/@v/"+ver+".mod", gomod, "text/plain")
|
||||
}
|
||||
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-go/files/"+mod+"/@v/v0.3.0.mod"), gomod, "text/plain", api("/api/v1/local/local-go/"+mod+"/@v/list"))
|
||||
|
||||
resp, body := doRequest(t, http.MethodPut, api("/api/v2/remotes/local-go/files/"+mod+"/@v/v0.3.0.zip"), []byte("junk"), "application/zip")
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Fatalf("invalid zip: status %d: %s", resp.StatusCode, body)
|
||||
|
||||
@@ -29,10 +29,10 @@ func TestLocalHelmRealClient(t *testing.T) {
|
||||
createVirtual(t, `{"name":"vhelm","package_type":"helm","members":["local-helm"]}`)
|
||||
defer deleteVirtual(t, "vhelm")
|
||||
|
||||
helm := func(t *testing.T, script string) string {
|
||||
helm := func(script string) string {
|
||||
t.Helper()
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken,
|
||||
"--entrypoint", "sh", "alpine/helm:3.16.4", "-c", "set -eu; "+script).Output()
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network, "--entrypoint", "sh",
|
||||
"alpine/helm:3.16.4", "-c", "set -eu; "+script).Output()
|
||||
if err != nil {
|
||||
stderr := ""
|
||||
if ee, ok := err.(*exec.ExitError); ok {
|
||||
@@ -43,7 +43,7 @@ func TestLocalHelmRealClient(t *testing.T) {
|
||||
return string(out)
|
||||
}
|
||||
|
||||
chart := helm(t, "cd /tmp && helm create e2echart >/dev/null && helm package e2echart >/dev/null && cat e2echart-0.1.0.tgz")
|
||||
chart := helm("cd /tmp && helm create e2echart >/dev/null && helm package e2echart >/dev/null && cat e2echart-0.1.0.tgz")
|
||||
uploadFile(t, "local-helm", "e2echart-0.1.0.tgz", []byte(chart), "application/gzip")
|
||||
|
||||
// Chart metadata is recorded asynchronously after upload; poll the index.
|
||||
@@ -58,32 +58,14 @@ func TestLocalHelmRealClient(t *testing.T) {
|
||||
if !strings.Contains(string(body), "e2echart-0.1.0.tgz") || !strings.Contains(string(body), "digest: ") {
|
||||
t.Fatalf("index.yaml never listed the chart:\n%s", body)
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-helm/files/e2echart-0.2.0.tgz"), []byte(chart), "application/gzip", api("/api/v1/local/local-helm/index.yaml"))
|
||||
|
||||
for _, repoURL := range []string{internal + "/api/v1/local/local-helm", internal + "/api/v1/virtual/vhelm"} {
|
||||
out := helm(t, "helm repo add e2e "+repoURL+" >/dev/null && "+
|
||||
"helm search repo e2e/e2echart && "+
|
||||
"cd /tmp && helm pull e2e/e2echart --version 0.1.0 && "+
|
||||
out := helm("helm repo add e2e " + repoURL + " >/dev/null && " +
|
||||
"helm search repo e2e/e2echart && " +
|
||||
"cd /tmp && helm pull e2e/e2echart --version 0.1.0 && " +
|
||||
"helm template rel e2echart-0.1.0.tgz | grep -c 'kind: Deployment'")
|
||||
if !strings.Contains(out, "e2e/e2echart") || !strings.Contains(out, "0.1.0") || !strings.HasSuffix(strings.TrimSpace(out), "1") {
|
||||
t.Fatalf("helm via %s:\n%s", repoURL, out)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("oci", func(t *testing.T) {
|
||||
t.Skip("helm (oras-go) fetches the registry token with an OAuth2 POST form the server ignores; fixed by unkin/artifactapi#180")
|
||||
createRepo(t, `{"name":"local-helm-oci","package_type":"docker","repo_type":"local"}`)
|
||||
defer deleteRepo(t, "local-helm-oci")
|
||||
reg := strings.TrimPrefix(internal, "http://")
|
||||
out := helm(t, "cd /tmp && helm create e2echart >/dev/null && helm package e2echart >/dev/null && "+
|
||||
"echo \"$BAD\" | helm registry login "+reg+" -u ci --password-stdin --insecure && "+
|
||||
"if helm push e2echart-0.1.0.tgz oci://"+reg+"/local-helm-oci --plain-http; then echo 'invalid token push accepted'; exit 1; fi && "+
|
||||
"echo \"$TOKEN\" | helm registry login "+reg+" -u ci --password-stdin --insecure && "+
|
||||
"helm push e2echart-0.1.0.tgz oci://"+reg+"/local-helm-oci --plain-http && "+
|
||||
"helm registry logout "+reg+" && rm -rf e2echart* && "+
|
||||
"helm pull oci://"+reg+"/local-helm-oci/e2echart --version 0.1.0 --plain-http && ls e2echart-0.1.0.tgz")
|
||||
if !strings.Contains(out, "e2echart-0.1.0.tgz") {
|
||||
t.Fatalf("helm OCI push/pull:\n%s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -7,67 +7,15 @@ package e2edocker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
jose "github.com/go-jose/go-jose/v4"
|
||||
"github.com/go-jose/go-jose/v4/jwt"
|
||||
)
|
||||
|
||||
// authToken is accepted by the stack's write gate; badToken carries the same
|
||||
// claims signed by a key the issuer never published.
|
||||
var authToken, badToken string
|
||||
|
||||
// TestMain publishes a fresh signing key through the static mockoidc issuer
|
||||
// (e2e-docker/oidc, served by docker-compose.e2e.yml) and mints both tokens.
|
||||
func TestMain(m *testing.M) {
|
||||
key := mustRSAKey()
|
||||
jwks, _ := json.Marshal(jose.JSONWebKeySet{Keys: []jose.JSONWebKey{{Key: &key.PublicKey, KeyID: "e2e", Algorithm: "RS256", Use: "sig"}}})
|
||||
if err := os.WriteFile(filepath.Join("oidc", "jwks.json"), jwks, 0o644); err != nil {
|
||||
log.Fatalf("write jwks: %v", err)
|
||||
}
|
||||
authToken, badToken = mintToken(key), mintToken(mustRSAKey())
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func mustRSAKey() *rsa.PrivateKey {
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
log.Fatalf("generate key: %v", err)
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func mintToken(key *rsa.PrivateKey) string {
|
||||
sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: jose.JSONWebKey{Key: key, KeyID: "e2e"}}, (&jose.SignerOptions{}).WithType("JWT"))
|
||||
if err != nil {
|
||||
log.Fatalf("signer: %v", err)
|
||||
}
|
||||
now := time.Now()
|
||||
tok, err := jwt.Signed(sig).Claims(jwt.Claims{
|
||||
Issuer: "http://mockoidc", Subject: "e2e", Audience: jwt.Audience{"artifactapi"},
|
||||
IssuedAt: jwt.NewNumericDate(now), Expiry: jwt.NewNumericDate(now.Add(time.Hour)),
|
||||
}).Serialize()
|
||||
if err != nil {
|
||||
log.Fatalf("sign token: %v", err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
|
||||
func basicAuth(token string) string {
|
||||
return "Basic " + base64.StdEncoding.EncodeToString([]byte("ci:"+token))
|
||||
}
|
||||
|
||||
func baseURL() string {
|
||||
if v := os.Getenv("ARTIFACTAPI_URL"); v != "" {
|
||||
return strings.TrimRight(v, "/")
|
||||
@@ -95,14 +43,7 @@ func fixtureBytes(t *testing.T, rel string) []byte {
|
||||
return b
|
||||
}
|
||||
|
||||
// doRequest sends authToken as a Bearer token; doRequestAuth sends authz
|
||||
// verbatim, or no Authorization header when it is empty.
|
||||
func doRequest(t *testing.T, method, url string, body []byte, contentType string) (*http.Response, []byte) {
|
||||
t.Helper()
|
||||
return doRequestAuth(t, method, url, body, contentType, "Bearer "+authToken)
|
||||
}
|
||||
|
||||
func doRequestAuth(t *testing.T, method, url string, body []byte, contentType, authz string) (*http.Response, []byte) {
|
||||
t.Helper()
|
||||
var r io.Reader
|
||||
if body != nil {
|
||||
@@ -115,9 +56,6 @@ func doRequestAuth(t *testing.T, method, url string, body []byte, contentType, a
|
||||
if contentType != "" {
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
}
|
||||
if authz != "" {
|
||||
req.Header.Set("Authorization", authz)
|
||||
}
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("%s %s: %v", method, url, err)
|
||||
@@ -168,17 +106,3 @@ func getEventually(t *testing.T, url string, timeout time.Duration) (*http.Respo
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// assertWriteGated checks a write is refused with no token and with a token
|
||||
// from an unknown key, and that readURL still serves anonymously.
|
||||
func assertWriteGated(t *testing.T, method, url string, body []byte, contentType, readURL string) {
|
||||
t.Helper()
|
||||
for _, authz := range []string{"", "Bearer " + badToken, basicAuth(badToken)} {
|
||||
if resp, b := doRequestAuth(t, method, url, body, contentType, authz); resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("%s %s with %.12q: status %d, want 401: %s", method, url, authz, resp.StatusCode, b)
|
||||
}
|
||||
}
|
||||
if resp, b := doRequestAuth(t, http.MethodGet, readURL, nil, "", ""); resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("anonymous GET %s: status %d: %s", readURL, resp.StatusCode, b)
|
||||
}
|
||||
}
|
||||
|
||||
+11
-25
@@ -15,11 +15,10 @@ import (
|
||||
"git.unkin.net/unkin/artifactapi/internal/testsupport"
|
||||
)
|
||||
|
||||
// uploadFile PUTs with Basic credentials, as `curl -u ci:$TOKEN` does.
|
||||
func uploadFile(t *testing.T, repo, filePath string, body []byte, contentType string) {
|
||||
t.Helper()
|
||||
url := api("/api/v2/remotes/" + repo + "/files/" + filePath)
|
||||
resp, respBody := doRequestAuth(t, http.MethodPut, url, body, contentType, basicAuth(authToken))
|
||||
resp, respBody := doRequest(t, http.MethodPut, url, body, contentType)
|
||||
if resp.StatusCode != http.StatusCreated {
|
||||
t.Fatalf("upload %s: status %d: %s", filePath, resp.StatusCode, respBody)
|
||||
}
|
||||
@@ -32,7 +31,6 @@ func TestLocalGenericUpload(t *testing.T) {
|
||||
|
||||
content := []byte("artifactapi local generic upload payload")
|
||||
uploadFile(t, "local-generic", "data/hello.bin", content, "application/octet-stream")
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-generic/files/data/other.bin"), content, "application/octet-stream", api("/api/v1/local/local-generic/data/hello.bin"))
|
||||
|
||||
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-generic/data/hello.bin"), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
@@ -43,18 +41,6 @@ func TestLocalGenericUpload(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestLocalTerraformUpload uploads a provider zip the way a release pipeline
|
||||
// does and downloads it back anonymously.
|
||||
func TestLocalTerraformUpload(t *testing.T) {
|
||||
createRepo(t, `{"name":"local-terraform","package_type":"terraform","repo_type":"local"}`)
|
||||
defer deleteRepo(t, "local-terraform")
|
||||
|
||||
const path = "unkin/e2e/terraform-provider-e2e_1.0.0_linux_amd64.zip"
|
||||
zip := []byte("PK\x05\x06" + strings.Repeat("\x00", 18))
|
||||
uploadFile(t, "local-terraform", path, zip, "application/zip")
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-terraform/files/unkin/e2e/terraform-provider-e2e_1.1.0_linux_amd64.zip"), zip, "application/zip", api("/api/v2/remotes/local-terraform/files/"+path))
|
||||
}
|
||||
|
||||
// TestLocalPyPIUpload uploads a wheel and validates the generated simple index.
|
||||
func TestLocalPyPIUpload(t *testing.T) {
|
||||
createRepo(t, `{"name":"local-pypi","package_type":"pypi","repo_type":"local"}`)
|
||||
@@ -62,7 +48,6 @@ func TestLocalPyPIUpload(t *testing.T) {
|
||||
|
||||
wheel := fixtureBytes(t, "packages/foo-1.0-py3-none-any.whl")
|
||||
uploadFile(t, "local-pypi", "foo-1.0-py3-none-any.whl", wheel, "application/zip")
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-pypi/files/foo-2.0-py3-none-any.whl"), wheel, "application/zip", api("/api/v1/local/local-pypi/simple/foo/"))
|
||||
|
||||
// Root index lists the package.
|
||||
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-pypi/simple/"), nil, "")
|
||||
@@ -110,7 +95,6 @@ func TestLocalRPMRepodata(t *testing.T) {
|
||||
if !strings.Contains(s, "<repomd") || !strings.Contains(s, "primary") {
|
||||
t.Fatalf("repomd.xml not a valid repodata document: %s", s)
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-rpm/files/e2e-testpkg-1.0-1.noarch.rpm"), rpm, "application/x-rpm", api("/api/v1/local/local-rpm/repodata/repomd.xml"))
|
||||
}
|
||||
|
||||
// TestLocalDebRepo uploads a .deb and validates that the flat apt index
|
||||
@@ -123,19 +107,23 @@ func TestLocalDebRepo(t *testing.T) {
|
||||
deb := testsupport.MinimalDeb("e2e-testpkg", "1.0.0", "amd64")
|
||||
uploadFile(t, "local-deb", "e2e-testpkg_1.0.0_amd64.deb", deb, "application/vnd.debian.binary-package")
|
||||
|
||||
// The index is generated asynchronously after upload; poll for it.
|
||||
resp, body := getEventually(t, api("/api/v1/local/local-deb/Packages"), 15*time.Second)
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Packages: status %d: %s", resp.StatusCode, body)
|
||||
// The index is served (empty) at once and lists the package only once the
|
||||
// upload is parsed asynchronously; poll until it does.
|
||||
var pkgs string
|
||||
for deadline := time.Now().Add(15 * time.Second); !strings.Contains(pkgs, "Package: e2e-testpkg") && time.Now().Before(deadline); time.Sleep(250 * time.Millisecond) {
|
||||
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-deb/Packages"), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Packages: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
pkgs = string(body)
|
||||
}
|
||||
pkgs := string(body)
|
||||
for _, want := range []string{"Package: e2e-testpkg", "Version: 1.0.0", "Architecture: amd64", "Filename: pool/e2e-testpkg_1.0.0_amd64.deb", "SHA256:"} {
|
||||
if !strings.Contains(pkgs, want) {
|
||||
t.Fatalf("Packages missing %q:\n%s", want, pkgs)
|
||||
}
|
||||
}
|
||||
|
||||
resp, body = doRequest(t, http.MethodGet, api("/api/v1/local/local-deb/Release"), nil, "")
|
||||
resp, body := doRequest(t, http.MethodGet, api("/api/v1/local/local-deb/Release"), nil, "")
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("Release: status %d: %s", resp.StatusCode, body)
|
||||
}
|
||||
@@ -154,7 +142,6 @@ func TestLocalDebRepo(t *testing.T) {
|
||||
if !bytes.Equal(body, deb) {
|
||||
t.Fatalf("deb content mismatch")
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-deb/files/e2e-testpkg_2.0.0_amd64.deb"), deb, "application/vnd.debian.binary-package", api("/api/v1/local/local-deb/Packages"))
|
||||
}
|
||||
|
||||
// TestLocalAlpineIndex uploads an .apk to an alpine local repo and validates
|
||||
@@ -191,7 +178,6 @@ func TestLocalAlpineIndex(t *testing.T) {
|
||||
if !bytes.Equal(body, apk) {
|
||||
t.Fatalf("apk content mismatch")
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-alpine/files/x86_64/e2e-testpkg-2.0-r0.apk"), apk, "application/vnd.android.package-archive", api("/api/v1/local/local-alpine/x86_64/APKINDEX.tar.gz"))
|
||||
}
|
||||
|
||||
func apkIndexText(t *testing.T, body []byte) string {
|
||||
|
||||
@@ -30,7 +30,7 @@ func TestLocalNPMPublishInstall(t *testing.T) {
|
||||
|
||||
reg := strings.TrimRight(internal, "/") + "/api/v1/local/" + name + "/"
|
||||
script := `set -euo pipefail
|
||||
printf 'registry=%s\n%s:_authToken=%s\n' "$REG" "${REG#http:}" "$TOKEN" > ~/.npmrc
|
||||
printf 'registry=%s\n%s:_authToken=unused\n' "$REG" "${REG#http:}" > ~/.npmrc
|
||||
mk() { d="/w/src/$1-$2"; mkdir -p "$d"; cd "$d"
|
||||
printf '{"name":"%s","version":"%s","main":"index.js"}' "$1" "$2" > package.json
|
||||
echo "module.exports = '$1@$2';" > index.js; }
|
||||
@@ -43,15 +43,10 @@ npm install --no-audit --cache /tmp/fresh-cache e2e-npm-plain @e2e/npm-scoped
|
||||
node -e "console.log('REQ', require('e2e-npm-plain'), require('@e2e/npm-scoped'))"
|
||||
mk e2e-npm-plain 1.1.0 && npm publish
|
||||
echo "VERSIONS $(npm view e2e-npm-plain versions --json | tr -d ' \n')"
|
||||
echo "LATEST $(npm view e2e-npm-plain dist-tags.latest)"
|
||||
printf 'registry=%s\n%s:_authToken=%s\n' "$REG" "${REG#http:}" "$BAD" > ~/.npmrc
|
||||
mk e2e-npm-plain 2.0.0
|
||||
if npm publish; then echo "invalid token publish accepted"; exit 1; fi
|
||||
printf 'registry=%s\n' "$REG" > ~/.npmrc
|
||||
echo "ANON $(npm view e2e-npm-plain dist-tags.latest)"`
|
||||
echo "LATEST $(npm view e2e-npm-plain dist-tags.latest)"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "REG="+reg, "-e", "TOKEN="+authToken, "-e", "BAD="+badToken, "node:22", "bash", "-c", script).CombinedOutput()
|
||||
"-e", "REG="+reg, "node:22", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("npm client run failed: %v\n%s", err, out)
|
||||
}
|
||||
@@ -59,8 +54,6 @@ echo "ANON $(npm view e2e-npm-plain dist-tags.latest)"`
|
||||
"REQ e2e-npm-plain@1.0.0 @e2e/npm-scoped@1.0.0",
|
||||
`VERSIONS ["1.0.0","1.1.0"]`,
|
||||
"LATEST 1.1.0",
|
||||
"code E401",
|
||||
"ANON 1.1.0",
|
||||
} {
|
||||
if !strings.Contains(string(out), want) {
|
||||
t.Fatalf("missing %q in npm output:\n%s", want, out)
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestVirtualNPMInstall(t *testing.T) {
|
||||
pub() { d="/w/src/$2-$3-$1"; mkdir -p "$d"; cd "$d"
|
||||
printf '{"name":"%s","version":"%s","main":"index.js"}' "$2" "$3" > package.json
|
||||
echo "module.exports = '$1:$2@$3';" > index.js
|
||||
printf 'registry=%s\n%s:_authToken=%s\n' "$BASE/api/v1/local/$1/" "${BASE#http:}/api/v1/local/$1/" "$TOKEN" > .npmrc
|
||||
printf 'registry=%s\n%s:_authToken=unused\n' "$BASE/api/v1/local/$1/" "${BASE#http:}/api/v1/local/$1/" > .npmrc
|
||||
npm publish; }
|
||||
pub "$A" @e2e/virt 1.0.0
|
||||
pub "$B" @e2e/virt 1.0.0
|
||||
@@ -51,7 +51,7 @@ node -e "console.log('REQ', require('@e2e/virt'))"
|
||||
echo "RESOLVED $(node -p "require('./package-lock.json').packages['node_modules/@e2e/virt'].resolved")"`
|
||||
|
||||
out, err := exec.Command("docker", "run", "--rm", "--network", network,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt, "-e", "TOKEN="+authToken,
|
||||
"-e", "BASE="+base, "-e", "A="+a, "-e", "B="+b, "-e", "V="+virt,
|
||||
"node:22", "bash", "-c", script).CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("npm client run failed: %v\n%s", err, out)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
{"issuer":"http://mockoidc","jwks_uri":"http://mockoidc/jwks.json","authorization_endpoint":"http://mockoidc/authorize","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"]}
|
||||
@@ -68,7 +68,6 @@ func TestLocalPuppetRealClient(t *testing.T) {
|
||||
if resp, body := getEventually(t, api("/api/v1/local/local-puppet/v3/releases/unkin-hello-0.1.0"), 15*time.Second); resp.StatusCode != http.StatusOK || !strings.Contains(string(body), `"file_md5"`) {
|
||||
t.Fatalf("release never indexed: %d %s", resp.StatusCode, body)
|
||||
}
|
||||
assertWriteGated(t, http.MethodPut, api("/api/v2/remotes/local-puppet/files/unkin-hello-0.2.0.tar.gz"), tarball, "application/gzip", api("/api/v1/local/local-puppet/v3/releases/unkin-hello-0.1.0"))
|
||||
|
||||
forge := internal + "/api/v1/local/local-puppet"
|
||||
run := func(image, script string) string {
|
||||
|
||||
@@ -17,8 +17,8 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
echo "==> building and starting stack (postgres, redis, minio, mockupstream(s), mockoidc, artifactapi)"
|
||||
"${COMPOSE[@]}" up -d --build postgres redis minio mockupstream mockupstreama mockupstreamb mockoidc artifactapi
|
||||
echo "==> building and starting stack (postgres, redis, minio, mockupstream(s), artifactapi)"
|
||||
"${COMPOSE[@]}" up -d --build postgres redis minio mockupstream mockupstreama mockupstreamb artifactapi
|
||||
|
||||
echo "==> waiting for artifactapi health at ${API_URL}"
|
||||
for i in $(seq 1 60); do
|
||||
|
||||
Reference in New Issue
Block a user