145 lines
5.3 KiB
Go
145 lines
5.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
registry "git.novox.be/novox/mesh-delivery/contracts/go/seats/containerimageregistry"
|
|
|
|
"git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
|
builds "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds"
|
|
)
|
|
|
|
// pushed are the blobs the fake holder took, by digest, for the tests that read an archive back.
|
|
var pushed sync.Map
|
|
|
|
// fakeStore is the registry seats' holder as m's publish meets it (publish.go; CAT-BN3): publish opens a transfer and
|
|
// answers the part it lacks, publish-part takes the chunks in sequence, and the last one keeps the whole part when it
|
|
// hashes to its digest. Each file is kept by name and digest; fail, when set, is the holder's refusal at the last chunk.
|
|
type fakeStore struct {
|
|
mu sync.Mutex
|
|
blobs map[string][]byte // "<name>@<digest>"
|
|
fail error
|
|
open map[string]registry.PublishInput
|
|
chunks map[string][]byte
|
|
published []registry.PublishInput
|
|
seats []string
|
|
}
|
|
|
|
func withStore(t *testing.T, m *Module) *fakeStore {
|
|
t.Helper()
|
|
s := &fakeStore{blobs: map[string][]byte{}, open: map[string]registry.PublishInput{}, chunks: map[string][]byte{}}
|
|
m.Call = func(key string, body any) (json.RawMessage, error) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
raw, _ := json.Marshal(body)
|
|
seat, verb, _ := strings.Cut(strings.TrimPrefix(key, "seat:"), ".")
|
|
if seat != buildstep.ArtifactSeat && seat != buildstep.NpmSeat {
|
|
return nil, errors.New("nats: no responders available for request")
|
|
}
|
|
switch verb {
|
|
case "publish":
|
|
var in registry.PublishInput
|
|
_ = json.Unmarshal(raw, &in)
|
|
if (seat == buildstep.ArtifactSeat) != (in.Kind == "bundle") || in.Transfer == "" || strings.ContainsAny(in.Transfer, "/. *>") {
|
|
t.Errorf("publish on %s of kind %q, transfer %q", seat, in.Kind, in.Transfer)
|
|
}
|
|
s.open[in.Transfer] = in
|
|
s.seats = append(s.seats, seat)
|
|
return json.Marshal(registry.PublishAnswer{Transfer: in.Transfer, Lacks: []string{in.Digest}})
|
|
case "publish-part":
|
|
var c registry.PublishPartInput
|
|
_ = json.Unmarshal(raw, &c)
|
|
data, _ := base64.StdEncoding.DecodeString(c.Data)
|
|
s.chunks[c.Transfer] = append(s.chunks[c.Transfer], data...)
|
|
if !c.Last {
|
|
return json.Marshal(registry.PublishPartAnswer{Transfer: c.Transfer, Part: c.Part})
|
|
}
|
|
in, whole := s.open[c.Transfer], s.chunks[c.Transfer]
|
|
delete(s.chunks, c.Transfer)
|
|
if s.fail != nil {
|
|
return nil, s.fail
|
|
}
|
|
if sum := sha256.Sum256(whole); "sha256:"+hex.EncodeToString(sum[:]) != in.Digest {
|
|
t.Errorf("published %s with bytes of another digest", in.Digest)
|
|
}
|
|
s.blobs[in.Name+"@"+in.Digest] = whole
|
|
s.published = append(s.published, in)
|
|
pushed.Store(in.Digest, whole)
|
|
return json.Marshal(registry.PublishPartAnswer{Transfer: c.Transfer, Part: c.Part, Complete: true})
|
|
}
|
|
return nil, errors.New("nats: no responders available for request")
|
|
}
|
|
return s
|
|
}
|
|
|
|
// archiveOf is the archive a build published, by its digest.
|
|
func archiveOf(t *testing.T, r BuildResult) []byte {
|
|
t.Helper()
|
|
raw, ok := pushed.Load(r.Digest)
|
|
if !ok {
|
|
t.Fatalf("nothing was published under %s", r.Digest)
|
|
}
|
|
return raw.([]byte)
|
|
}
|
|
|
|
// 1587-S6 (ADR 0498 §1; CAT-BN3): a done bundle is published through oci-artifact-registry and answers the seat's
|
|
// reference of its archive, under the manifest's module.
|
|
func TestADoneBundleAnswersTheReferenceOfTheArchiveItPushed(t *testing.T) {
|
|
m, _, _ := builtMachine(t, ``)
|
|
store := withStore(t, m)
|
|
out, err := m.build(context.Background(), buildArgs())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
r := out.(BuildResult)
|
|
if want := "oci-artifact-registry/m/tools@" + r.Digest; r.Reference != want {
|
|
t.Errorf("reference %q, want %q", r.Reference, want)
|
|
}
|
|
if store.blobs["m/tools@"+r.Digest] == nil || len(store.seats) != 1 || store.seats[0] != buildstep.ArtifactSeat {
|
|
t.Errorf("the holder holds %d files through %v, not m/tools@%s through %s", len(store.blobs), store.seats, r.Digest, buildstep.ArtifactSeat)
|
|
}
|
|
}
|
|
|
|
// 1587-S7 and S8 (ADR 0498 §2, the lead's decision L9): a publish the holder refuses fails the step `not stored:
|
|
// <artifact>: <the holder's words>`, a seat nothing holds too, and whatever it answered, no packed file is left.
|
|
func TestARefusedPushFailsNotStoredAndNoPackedFileStays(t *testing.T) {
|
|
for _, c := range []struct {
|
|
fail error
|
|
noSeat bool
|
|
words string
|
|
}{{nil, false, ""}, {errors.New("refused: the store did not take it: 507 Insufficient Storage"), false, "507"},
|
|
{nil, true, "no responders"}} {
|
|
m, _, work := builtMachine(t, ``)
|
|
withStore(t, m).fail = c.fail
|
|
if c.noSeat {
|
|
m.Call = func(string, any) (json.RawMessage, error) {
|
|
return nil, errors.New("nats: no responders available for request")
|
|
}
|
|
}
|
|
_, err := m.build(context.Background(), buildArgs())
|
|
if c.words != "" && (err == nil || !strings.HasPrefix(err.Error(), builds.NotStored+"tools: ") || !strings.Contains(err.Error(), c.words)) {
|
|
t.Errorf("a refused publish answered %v", err)
|
|
}
|
|
var archives []string
|
|
filepath.WalkDir(work, func(p string, d os.DirEntry, err error) error {
|
|
if err == nil && strings.HasSuffix(p, ".tar.gz") {
|
|
archives = append(archives, p)
|
|
}
|
|
return nil
|
|
})
|
|
if len(archives) != 0 {
|
|
t.Errorf("publish answered %v: packed files left on the node: %v", c.fail, archives)
|
|
}
|
|
}
|
|
}
|