174 lines
6.9 KiB
Go
174 lines
6.9 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
const head = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
type fakeMsg struct {
|
|
subject string
|
|
data []byte
|
|
acked, termd int
|
|
nak []time.Duration
|
|
}
|
|
|
|
func (f *fakeMsg) Subject() string { return f.subject }
|
|
func (f *fakeMsg) Data() []byte { return f.data }
|
|
func (f *fakeMsg) Headers() nats.Header { return nats.Header{"X-Event-Id": {"ev-1"}} }
|
|
func (f *fakeMsg) Ack() error { f.acked++; return nil }
|
|
func (f *fakeMsg) NakWithDelay(d time.Duration) error { f.nak = append(f.nak, d); return nil }
|
|
func (f *fakeMsg) Term() error { f.termd++; return nil }
|
|
|
|
type fakePub struct {
|
|
subjects []string
|
|
data [][]byte
|
|
err error
|
|
}
|
|
|
|
func (p *fakePub) Publish(_ context.Context, subject string, payload []byte, _ ...jetstream.PublishOpt) (*jetstream.PubAck, error) {
|
|
if p.err != nil {
|
|
return nil, p.err
|
|
}
|
|
p.subjects, p.data = append(p.subjects, subject), append(p.data, payload)
|
|
return &jetstream.PubAck{}, nil
|
|
}
|
|
|
|
// body is gitea.pull.updated as mesh-catalog modules/gitea/trunk.ts:233-250 writes it (the fields the bridge reads).
|
|
func body(m map[string]any) []byte {
|
|
base := map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "head": "feat/x", "head_sha": head,
|
|
"module_dirs": []string{"modules/a"}, "module_dirs_said": true}
|
|
for k, v := range m {
|
|
if v == nil {
|
|
delete(base, k)
|
|
} else {
|
|
base[k] = v
|
|
}
|
|
}
|
|
b, _ := json.Marshal(base)
|
|
return b
|
|
}
|
|
|
|
func TestReadableEventIsWrittenOnForgeAndAcked(t *testing.T) { // 1235/AC2 (unit part; the bus is the integration test)
|
|
var logs bytes.Buffer
|
|
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.updated", data: body(nil)}, &fakePub{}
|
|
handle(context.Background(), m, pub, nil, log.New(&logs, "", 0))
|
|
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.pull" {
|
|
t.Fatalf("published on %v", pub.subjects)
|
|
}
|
|
f, err := delivery.DecodeForge(pub.data[0])
|
|
if err != nil || f.Number != 12 || f.Branch != "feat/x" || f.Head != head || f.Action != "updated" || !f.DirsKnown {
|
|
t.Errorf("message %+v err %v", f, err)
|
|
}
|
|
if m.acked != 1 || m.termd != 0 {
|
|
t.Errorf("acked %d terminated %d", m.acked, m.termd)
|
|
}
|
|
}
|
|
|
|
func TestUnreadableEventIsTerminatedAndLogged(t *testing.T) { // 1235/AC5
|
|
var logs bytes.Buffer
|
|
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.updated", data: body(map[string]any{"head_sha": "short"})}, &fakePub{}
|
|
handle(context.Background(), m, pub, nil, log.New(&logs, "", 0))
|
|
if len(pub.subjects) != 0 {
|
|
t.Errorf("a message was written: %v", pub.subjects)
|
|
}
|
|
if m.termd != 1 || m.acked != 0 {
|
|
t.Errorf("terminated %d acked %d, want terminated once", m.termd, m.acked)
|
|
}
|
|
if !strings.Contains(logs.String(), "mesh.mod.gitea.event.pull.updated") || !strings.Contains(logs.String(), "head") {
|
|
t.Errorf("log %q names neither the subject nor why", logs.String())
|
|
}
|
|
}
|
|
|
|
func TestUnwrittenMessageIsNotAcked(t *testing.T) { // 1235 (a failed write is redelivered by the server, not retried here)
|
|
var logs bytes.Buffer
|
|
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.updated", data: body(nil)}, &fakePub{err: errors.New("no stream")}
|
|
handle(context.Background(), m, pub, nil, log.New(&logs, "", 0))
|
|
if m.acked != 0 || m.termd != 0 {
|
|
t.Errorf("acked %d terminated %d, want neither", m.acked, m.termd)
|
|
}
|
|
if !strings.Contains(logs.String(), "no stream") {
|
|
t.Errorf("log %q", logs.String())
|
|
}
|
|
}
|
|
|
|
// mergedEvent is gitea.pull.merged as mesh-catalog modules/gitea/index.ts:186-207 writes it (the fields the bridge reads).
|
|
func mergedEvent() []byte {
|
|
b, _ := json.Marshal(map[string]any{"owner": "novox", "repo": "mesh-delivery", "number": 12, "head": "feat/x", "head_sha": head,
|
|
"base": "main", "merge_commit_sha": "89abcdef0123456789abcdef0123456789abcdef"})
|
|
return b
|
|
}
|
|
|
|
func TestTreesRefusedLeavesTheEventForTenSecondsAndLogs(t *testing.T) { // 1236/AC3
|
|
var logs bytes.Buffer
|
|
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.merged", data: mergedEvent()}, &fakePub{}
|
|
call := func(context.Context, string, any) (json.RawMessage, error) {
|
|
return nil, errors.New("the tree listing of novox/mesh-delivery was cut")
|
|
}
|
|
handle(context.Background(), m, pub, treesVia(context.Background(), call), log.New(&logs, "", 0))
|
|
if len(pub.subjects) != 0 {
|
|
t.Errorf("a message was written: %v", pub.subjects)
|
|
}
|
|
if m.acked != 0 || m.termd != 0 || len(m.nak) != 1 || m.nak[0] < 10*time.Second {
|
|
t.Errorf("acked %d terminated %d naks %v, want one nak of at least 10 s", m.acked, m.termd, m.nak)
|
|
}
|
|
for _, w := range []string{"novox/mesh-delivery", "89abcdef0123456789abcdef0123456789abcdef", "the tree listing of novox/mesh-delivery was cut"} {
|
|
if !strings.Contains(logs.String(), w) {
|
|
t.Errorf("log %q does not name %q", logs.String(), w)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMergeCallsTheToolOnceAndWritesTheMergedMessage(t *testing.T) { // 1236/AC1 (through the tool's request and answer)
|
|
var logs bytes.Buffer
|
|
m, pub := &fakeMsg{subject: "mesh.mod.gitea.event.pull.merged", data: mergedEvent()}, &fakePub{}
|
|
var keys []string
|
|
var args []any
|
|
call := func(_ context.Context, key string, body any) (json.RawMessage, error) {
|
|
keys, args = append(keys, key), append(args, body)
|
|
return json.RawMessage(`{"root":"r0","trees":{"":"t0","modules/a":"t1"}}`), nil
|
|
}
|
|
handle(context.Background(), m, pub, treesVia(context.Background(), call), log.New(&logs, "", 0))
|
|
if len(keys) != 1 || keys[0] != "gitea.gitea_commit_trees" {
|
|
t.Fatalf("calls %v", keys)
|
|
}
|
|
if got, _ := json.Marshal(args[0]); string(got) != `{"owner":"novox","repo":"mesh-delivery","sha":"89abcdef0123456789abcdef0123456789abcdef"}` {
|
|
t.Errorf("arguments %s", got)
|
|
}
|
|
if len(pub.subjects) != 1 || pub.subjects[0] != "mesh.stream.mesh-delivery-forge-bridge.event.forge.novox.mesh-delivery.merged" {
|
|
t.Fatalf("published on %v", pub.subjects)
|
|
}
|
|
f, err := delivery.DecodeForge(pub.data[0])
|
|
if err != nil || f.Root != "r0" || f.Trees[""] != "t0" || f.Trees["modules/a"] != "t1" || f.Merge == "" {
|
|
t.Errorf("message %+v err %v", f, err)
|
|
}
|
|
if m.acked != 1 {
|
|
t.Errorf("acked %d", m.acked)
|
|
}
|
|
}
|
|
|
|
func TestAnAnswerWithoutARootIsNotUsed(t *testing.T) { // 1236 (a shape the tool does not answer)
|
|
call := func(context.Context, string, any) (json.RawMessage, error) {
|
|
return json.RawMessage(`{"trees":{"a":"t"}}`), nil
|
|
}
|
|
if _, _, err := treesVia(context.Background(), call)("o", "r", "s"); err == nil {
|
|
t.Error("an answer with no root was taken")
|
|
}
|
|
call = func(context.Context, string, any) (json.RawMessage, error) { return json.RawMessage(`[1]`), nil }
|
|
if _, _, err := treesVia(context.Background(), call)("o", "r", "s"); err == nil {
|
|
t.Error("an answer that is not an object was taken")
|
|
}
|
|
}
|