The Go bundle raises packages-token-refused in the module's findings state when the forge answers 401 to a write; the watcher mints afresh on that key's event and clears it (lead's ruling on A3n, map-onto-how-the-mesh-is-built: nothing waits on a timer). Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
198 lines
8.3 KiB
Go
198 lines
8.3 KiB
Go
package main
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"bytes"
|
|
"compress/gzip"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/health"
|
|
"gitea/internal/forgehttp"
|
|
"gitea/internal/packageseats"
|
|
)
|
|
|
|
func tarball(t *testing.T, manifest string) []byte {
|
|
var b bytes.Buffer
|
|
gz := gzip.NewWriter(&b)
|
|
tw := tar.NewWriter(gz)
|
|
_ = tw.WriteHeader(&tar.Header{Name: "package/package.json", Mode: 0o644, Size: int64(len(manifest))})
|
|
_, _ = tw.Write([]byte(manifest))
|
|
_ = tw.Close()
|
|
_ = gz.Close()
|
|
return b.Bytes()
|
|
}
|
|
|
|
func moduleZip(t *testing.T, prefix string) []byte {
|
|
var b bytes.Buffer
|
|
z := zip.NewWriter(&b)
|
|
w, _ := z.Create(prefix + "/go.mod")
|
|
_, _ = w.Write([]byte("module x\n"))
|
|
_ = z.Close()
|
|
return b.Bytes()
|
|
}
|
|
|
|
func sha(b []byte) string { s := sha256.Sum256(b); return hex.EncodeToString(s[:]) }
|
|
|
|
// registry is the forge's package registry: what was uploaded, by path, and what the files API answers.
|
|
type registry struct {
|
|
mu sync.Mutex
|
|
uploaded map[string][]byte // "npm <name>" or "go"
|
|
held map[string][]byte // "<type>/<name>/<version>" -> file
|
|
deleted []string
|
|
}
|
|
|
|
func (g *registry) handle(w http.ResponseWriter, r *http.Request) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
body, _ := io.ReadAll(r.Body)
|
|
p := r.URL.EscapedPath()
|
|
// Reads come as the tools' account (read:package), writes as the packages account (write:package), never the admin.
|
|
want := "token reader"
|
|
if r.Method != http.MethodGet {
|
|
want = "token writer"
|
|
}
|
|
if got := r.Header.Get("Authorization"); got == "token stale" {
|
|
http.Error(w, "unauthorized", http.StatusUnauthorized)
|
|
return
|
|
} else if got != want {
|
|
http.Error(w, r.Method+" "+p+" came as "+got+", not "+want, http.StatusForbidden)
|
|
return
|
|
}
|
|
switch {
|
|
case r.Method == http.MethodPut && p == "/api/packages/acme/go/upload":
|
|
if r.Header.Get("Content-Type") != "application/zip" {
|
|
http.Error(w, "not a zip", 400)
|
|
return
|
|
}
|
|
g.uploaded["go"] = body
|
|
w.WriteHeader(http.StatusCreated)
|
|
case r.Method == http.MethodPut && strings.HasPrefix(p, "/api/packages/acme/npm/"):
|
|
g.uploaded["npm "+strings.TrimPrefix(p, "/api/packages/acme/npm/")] = body
|
|
w.WriteHeader(http.StatusCreated)
|
|
case strings.HasPrefix(p, "/api/v1/packages/acme/") && strings.HasSuffix(p, "/files"):
|
|
f, ok := g.held[strings.TrimSuffix(strings.TrimPrefix(r.URL.Path, "/api/v1/packages/acme/"), "/files")]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_ = json.NewEncoder(w).Encode([]map[string]any{{"sha256": sha(f), "size": len(f)}})
|
|
case r.Method == http.MethodGet && p == "/api/v1/packages/acme":
|
|
var out []map[string]any
|
|
for k := range g.held {
|
|
parts := strings.Split(k, "/")
|
|
if parts[0] == r.URL.Query().Get("type") {
|
|
out = append(out, map[string]any{"name": strings.Join(parts[1:len(parts)-1], "/"), "version": parts[len(parts)-1], "created_at": time.Unix(int64(len(parts[len(parts)-1])), 0)})
|
|
}
|
|
}
|
|
_ = json.NewEncoder(w).Encode(out)
|
|
case r.Method == http.MethodDelete:
|
|
g.deleted = append(g.deleted, strings.TrimPrefix(p, "/api/v1/packages/acme/"))
|
|
w.WriteHeader(http.StatusNoContent)
|
|
default:
|
|
http.Error(w, "unexpected "+r.Method+" "+p, 500)
|
|
}
|
|
}
|
|
|
|
// **The package seats store into the forge's package registry** (design 51 *Publishing*): an npm package goes up as
|
|
// npm's publish sends it, its package.json from the tarball and its tag as its dist-tag; a Go module's zip goes up to
|
|
// the Go upload, its module path read from the zip; held, versions and remove read and delete through the forge's
|
|
// package API, a Go version with Go's leading v.
|
|
func TestThePackageSeatsStoreIntoTheForge(t *testing.T) {
|
|
g := ®istry{uploaded: map[string][]byte{}, held: map[string][]byte{}}
|
|
srv := httptest.NewServer(http.HandlerFunc(g.handle))
|
|
defer srv.Close()
|
|
dir := t.TempDir()
|
|
kept := func(name, token, line string) string {
|
|
file := filepath.Join(dir, name)
|
|
_ = os.WriteFile(file, []byte(token+"\n"), 0o600)
|
|
_ = os.WriteFile(file+".scopes", []byte(line+"\n"), 0o600)
|
|
return file
|
|
}
|
|
reader := forgehttp.Client{URL: srv.URL, TokenFile: kept("token", "reader", "read:package,read:user for mesh-tools"), Account: "mesh-tools"}
|
|
writer := forgehttp.Client{URL: srv.URL, TokenFile: kept("packages-token", "writer", "write:package for mesh-packages"), Account: "mesh-packages"}
|
|
forge := &Forge{Client: reader, Writer: &writer, Owner: "acme"}
|
|
if _, err := (seatStore{forge: func() (*Forge, error) { return &Forge{Client: reader, Owner: "acme"}, nil }}).Put(packageseats.GoSeat, packageseats.Publish{}, nil); err == nil ||
|
|
!strings.Contains(err.Error(), "write side") {
|
|
t.Errorf("a forge handed no write side wrote: %v", err)
|
|
}
|
|
s := seatStore{forge: func() (*Forge, error) { return forge, nil }}
|
|
// The forge refuses the write side's token: the finding the TypeScript bundle renews on is raised under its key,
|
|
// and the next write the forge takes clears it (no timer looks at the token).
|
|
var refused []bool
|
|
forge.Refused = func(raised bool) { refused = append(refused, raised) }
|
|
if k := health.FindingKey(health.ScopeSeat, packageseats.Self, PackagesTokenRefused+".write"); k != PackagesTokenRefusedKey {
|
|
t.Errorf("the refusal's key is %s, the watcher watches %s", k, PackagesTokenRefusedKey)
|
|
}
|
|
_ = os.WriteFile(writer.TokenFile, []byte("stale\n"), 0o600)
|
|
if _, err := s.Put(packageseats.GoSeat, packageseats.Publish{Version: "0.1.15"}, moduleZip(t, "git.example/acme/sdk@v0.1.15")); err == nil ||
|
|
len(refused) != 1 || !refused[0] {
|
|
t.Errorf("a write the forge refused with 401 raised no finding: %v %v", err, refused)
|
|
}
|
|
_ = os.WriteFile(writer.TokenFile, []byte("writer\n"), 0o600)
|
|
|
|
tgz := tarball(t, `{"name":"@acme/contracts","version":"0.1.15-feat-x.2","main":"index.js"}`)
|
|
p := packageseats.Publish{Name: "@acme/contracts", Version: "0.1.15-feat-x.2", Tag: "feat-x", Kind: packageseats.KindPackage, Digest: "sha256:" + sha(tgz)}
|
|
if module, err := s.Put(packageseats.NpmSeat, p, tgz); err != nil || module != "" {
|
|
t.Fatalf("npm put: %q, %v", module, err)
|
|
}
|
|
var doc struct {
|
|
DistTags map[string]string `json:"dist-tags"`
|
|
Versions map[string]map[string]any
|
|
Attachments map[string]struct{ Data string } `json:"_attachments"`
|
|
}
|
|
if err := json.Unmarshal(g.uploaded["npm @acme%2Fcontracts"], &doc); err != nil {
|
|
t.Fatalf("npm's publish body %v: %s", err, g.uploaded)
|
|
}
|
|
att := doc.Attachments["contracts-0.1.15-feat-x.2.tgz"].Data
|
|
if doc.DistTags["feat-x"] != "0.1.15-feat-x.2" || doc.DistTags["latest"] != "" || doc.Versions["0.1.15-feat-x.2"]["main"] != "index.js" ||
|
|
att != base64.StdEncoding.EncodeToString(tgz) {
|
|
t.Errorf("npm's publish body: %+v", doc)
|
|
}
|
|
p.Name = "@acme/other"
|
|
if _, err := s.Put(packageseats.NpmSeat, p, tgz); err == nil || !strings.Contains(err.Error(), "not @acme/other") {
|
|
t.Errorf("a tarball of another package was uploaded: %v", err)
|
|
}
|
|
|
|
zipped := moduleZip(t, "git.example/acme/sdk@v0.1.15")
|
|
gp := packageseats.Publish{Name: "git.example/acme/sdk", Version: "0.1.15", Kind: packageseats.KindPackage, Digest: "sha256:" + sha(zipped)}
|
|
if module, err := s.Put(packageseats.GoSeat, gp, zipped); err != nil || module != "git.example/acme/sdk" || !bytes.Equal(g.uploaded["go"], zipped) {
|
|
t.Fatalf("go put: %q, %v", module, err)
|
|
}
|
|
gp.Version = "0.1.16"
|
|
if _, err := s.Put(packageseats.GoSeat, gp, zipped); err == nil {
|
|
t.Error("a zip of another version was uploaded")
|
|
}
|
|
|
|
g.held["go/git.example/acme/sdk/v0.1.15"] = zipped
|
|
if d, ok, err := s.Held(packageseats.GoSeat, "git.example/acme/sdk", "0.1.15"); err != nil || !ok || d != "sha256:"+sha(zipped) {
|
|
t.Errorf("held: %s %v %v", d, ok, err)
|
|
}
|
|
if _, ok, err := s.Held(packageseats.NpmSeat, "x", "0.1.0"); ok || err != nil {
|
|
t.Errorf("a version the forge does not hold: %v %v", ok, err)
|
|
}
|
|
g.held["go/git.example/acme/sdk/v0.1.9"] = zipped
|
|
if v, err := s.Versions(packageseats.GoSeat, "git.example/acme/sdk"); err != nil || len(v) != 2 || v[0].Version != "0.1.15" {
|
|
t.Errorf("versions: %+v %v", v, err)
|
|
}
|
|
if err := s.Remove(packageseats.GoSeat, "git.example/acme/sdk", "0.1.15-feat.1"); err != nil || len(g.deleted) != 1 ||
|
|
g.deleted[0] != "go/git.example%2Facme%2Fsdk/v0.1.15-feat.1" {
|
|
t.Errorf("remove: %v %v", err, g.deleted)
|
|
}
|
|
if len(refused) < 2 || refused[len(refused)-1] {
|
|
t.Errorf("a write the forge took after the refusal did not clear it: %v", refused)
|
|
}
|
|
}
|