Files
mesh-sdk/go/publish/publish_test.go
T
jschoubben 0bb3a2c392 SDK-1: the runtime sends a file to a registry seat in parts and goes on from what the holder holds (ADR 0611, design 51)
publish.Publish opens the transfer naming every part's digest and sends only the parts the holder answers it lacks, in
chunks of at most 760 KiB (a message under the bus's 1 MiB once base64), in sequence with the last marked; a part
whose bytes are not its digest is refused before any of it travels. The seat's messages are mesh-contracts'; the call
is the caller's own. Its test takes the place of the vault's get test, merged into the vault's table.

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

123 lines
4.5 KiB
Go

package publish
import (
"bytes"
"context"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"io"
"strings"
"testing"
"git.novox.be/novox/mesh-contracts/go/parts/registry"
)
// 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)
}
}