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-contracts/health-and-repair/go/clients/meshconditionsclient" "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 " or "go" held map[string][]byte // "//" -> 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 := meshconditionsclient.Key(meshconditionsclient.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) } }