Files
mesh-sdk/go/publish/publish_test.go
T
jschoubben e39faa0f38 SDK-1: the parts helper reads the registry part's shapes from a registry seat's module
Interim, for the lead's ruling: ADR 0625 §1 cuts the registry part into each registry seat's own Go module, and §3
lets no mesh-sdk package but catalogue import a contracts module; this helper is logic shared over a seat, a client
in §4's sense, which no clause lets the registry milestone build. Kept building meanwhile.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 12:54:26 +01:00

123 lines
4.5 KiB
Go

package publish
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"strings"
"testing"
registry "git.novox.be/novox/mesh-contracts/go/seats/containerimageregistry"
)
// A file travels to a registry seat in parts (ADR 0611, design 51 *Publishing*; SDK-1): only the parts the holder says
// it lacks are read and sent, each in chunks of at most MaxChunk in sequence with the last marked, so a cut transfer
// opened again goes on from what the holder holds; a part whose bytes are not its digest is refused before any of it
// travels.
func TestAFileTravelsInTheParts(t *testing.T) {
big := bytes.Repeat([]byte("x"), MaxChunk+10)
small, empty := []byte("config"), []byte{}
part := func(b []byte, opened *int) Part {
sum := sha256.Sum256(b)
return Part{Digest: "sha256:" + hex.EncodeToString(sum[:]), Size: int64(len(b)),
Open: func() (io.ReadCloser, error) { *opened++; return io.NopCloser(bytes.NewReader(b)), nil }}
}
type chunk struct {
part string
seq int64
last bool
data []byte
}
holder := func(lacks []string, got *[]chunk, asked *[]string) Call {
return func(key string, body any) (json.RawMessage, error) {
*asked = append(*asked, key)
raw, _ := json.Marshal(body)
switch key {
case "seat:oci-artifact-registry.publish":
return json.Marshal(registry.PublishAnswer{Transfer: "t1", Lacks: lacks, Held: len(lacks) == 0})
case "seat:oci-artifact-registry.publish-part":
var in registry.PublishPartInput
_ = json.Unmarshal(raw, &in)
data, _ := base64.StdEncoding.DecodeString(in.Data)
if len(raw) > 1<<20 {
t.Errorf("a chunk's message is %d bytes, over the bus's largest", len(raw))
}
*got = append(*got, chunk{in.Part, in.Sequence, in.Last, data})
return json.Marshal(registry.PublishPartAnswer{Transfer: in.Transfer, Part: in.Part})
}
t.Fatalf("asked %s", key)
return nil, nil
}
}
in := registry.PublishInput{Transfer: "t1", Name: "files", Kind: "archive", Digest: "sha256:" + strings.Repeat("a", 64)}
var opens [3]int
parts := []Part{part(big, &opens[0]), part(small, &opens[1]), part(empty, &opens[2])}
for _, c := range []struct {
name string
lacks []int
chunks int
err string
}{
{"a fresh transfer sends every part, the big one in two chunks and the empty one as one", []int{0, 1, 2}, 4, ""},
{"a cut transfer opened again sends only what the holder lacks", []int{2}, 1, ""},
{"a digest the holder holds sends nothing", nil, 0, ""},
} {
opens = [3]int{}
var lacks []string
for _, i := range c.lacks {
lacks = append(lacks, parts[i].Digest)
}
var got []chunk
var asked []string
sent, err := Publish(context.Background(), holder(lacks, &got, &asked), "oci-artifact-registry", in, parts)
if err != nil || sent.Chunks != c.chunks || len(got) != c.chunks || len(sent.Parts) != len(c.lacks) {
t.Errorf("%s: sent %+v, %d chunks heard, %v", c.name, sent, len(got), err)
continue
}
for i := range parts {
want := 0
for _, l := range c.lacks {
if l == i {
want = 2 // read once to check its digest, once to send it
}
}
if opens[i] != want {
t.Errorf("%s: part %d read %d times, want %d", c.name, i, opens[i], want)
}
}
whole := map[string][]byte{}
for i, ch := range got {
whole[ch.part] = append(whole[ch.part], ch.data...)
lastOfPart := i == len(got)-1 || got[i+1].part != ch.part
if ch.last != lastOfPart || (i > 0 && got[i-1].part == ch.part && got[i-1].seq+1 != ch.seq) {
t.Errorf("%s: chunk %d of %s is %d, last %v", c.name, i, ch.part[:12], ch.seq, ch.last)
}
}
for _, l := range c.lacks {
if p := parts[l]; int64(len(whole[p.Digest])) != p.Size {
t.Errorf("%s: part %d arrived as %d bytes of %d", c.name, l, len(whole[p.Digest]), p.Size)
}
}
}
// A part whose bytes are not its digest travels not at all; a part the holder names that the transfer does not is
// refused, naming it.
liar := part(small, &opens[1])
liar.Digest = parts[0].Digest
var got []chunk
var asked []string
if _, err := Publish(context.Background(), holder([]string{liar.Digest}, &got, &asked), "oci-artifact-registry", in,
[]Part{liar}); err == nil || len(got) != 0 || !strings.Contains(err.Error(), "nothing of it is sent") {
t.Errorf("a part not its digest: %v, %d chunks sent", err, len(got))
}
if _, err := Publish(context.Background(), holder([]string{"sha256:" + strings.Repeat("f", 64)}, &got, &asked),
"oci-artifact-registry", in, parts); err == nil || !strings.Contains(err.Error(), "does not name") {
t.Errorf("a part the transfer does not name: %v", err)
}
}