Run the dockerised e2e suite with write auth enabled
ci/woodpecker/pr/pre-commit Pipeline was successful
ci/woodpecker/pr/test Pipeline was successful
ci/woodpecker/pr/build Pipeline was successful

Real clients now authenticate every local publish against a static mock
OIDC issuer, and each type checks unauthenticated publishes are refused
while anonymous reads keep working.
This commit is contained in:
2026-11-02 22:01:44 +11:00
parent f8d6db3bd2
commit a161c44575
17 changed files with 249 additions and 34 deletions
+29 -7
View File
@@ -15,10 +15,11 @@ 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 := doRequest(t, http.MethodPut, url, body, contentType)
resp, respBody := doRequestAuth(t, http.MethodPut, url, body, contentType, basicAuth(authToken))
if resp.StatusCode != http.StatusCreated {
t.Fatalf("upload %s: status %d: %s", filePath, resp.StatusCode, respBody)
}
@@ -31,6 +32,7 @@ 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 {
@@ -41,6 +43,18 @@ 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"}`)
@@ -48,6 +62,7 @@ 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, "")
@@ -95,6 +110,7 @@ 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
@@ -107,19 +123,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)
}
@@ -138,6 +158,7 @@ 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
@@ -174,6 +195,7 @@ 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 {