package knotfeed
import (
"bytes"
"encoding/hex"
"slices"
"strings"
"testing"
"github.com/bluesky-social/indigo/atproto/syntax"
cbg "github.com/whyrusleeping/cbor-gen"
)
const frozenPushedHex = "a4637368617828616261626162616261626162616261626162616261626162616261626162616261626162616261626524747970657273682e74616e676c65642e6769742e7265666d782d746e676c2d656469746f726e6469643a706c633a6c696d70657473782d746e676c2d707573682d6f7074696f6e738167736b69702d6369"
const frozenBareHex = "a2637368617840636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646364636463646524747970657273682e74616e676c65642e6769742e726566"
func TestFrozenRecordBytesDecodeAndEncodeBackAsThemselves(t *testing.T) {
for _, tt := range []struct {
name string
hex string
want RefRecord
}{
{"pushed", frozenPushedHex, RefRecord{
Sha: sha(t, strings.Repeat("ab", 20)), Editor: "did:plc:limpet", PushOptions: []string{"skip-ci"},
}},
{"catch-up", frozenBareHex, RefRecord{Sha: sha(t, strings.Repeat("cd", 32))}},
} {
t.Run(tt.name, func(t *testing.T) {
data, err := hex.DecodeString(tt.hex)
if err != nil {
t.Fatalf("frozen hex: %v", err)
}
rec, err := DecodeRefRecord(data)
if err != nil {
t.Fatalf("DecodeRefRecord: %v", err)
}
if rec.Sha != tt.want.Sha || rec.Editor != tt.want.Editor ||
!slices.Equal(rec.PushOptions, tt.want.PushOptions) {
t.Fatalf("record = %+v, want %+v", rec, tt.want)
}
again, err := encodeRefRecord(tt.want)
if err != nil {
t.Fatalf("encodeRefRecord: %v", err)
}
if !bytes.Equal(again, data) {
t.Errorf("re-encoded record\n got %x\nwant %x", again, data)
}
})
}
}
func shaRecord(t *testing.T, reported string) []byte {
t.Helper()
var out bytes.Buffer
if err := cbg.CborWriteHeader(&out, cbg.MajMap, 1); err != nil {
t.Fatalf("map header: %v", err)
}
writeText(&out, "sha")
writeText(&out, reported)
return out.Bytes()
}
func TestDecodeRefRecordRefusesMalformedRecords(t *testing.T) {
const overlongShaRun = "a2637368617840636463646364636463646364636463646364636463646364636463" +
"64636463646364636463646364636463646364636463646364636463646364636463646364636463" +
"646524747970657273682e74616e676c65642e6769742e726566"
overlong, err := hex.DecodeString(overlongShaRun)
if err != nil {
t.Fatalf("decoding overlong record: %v", err)
}
var hugeArray bytes.Buffer
if err := cbg.CborWriteHeader(&hugeArray, cbg.MajMap, 1); err != nil {
t.Fatalf("map header: %v", err)
}
writeText(&hugeArray, "x-tngl-push-options")
if err := cbg.CborWriteHeader(&hugeArray, cbg.MajArray, 1<<32); err != nil {
t.Fatalf("array header: %v", err)
}
refused := [][]byte{{0xa1, 0x63, 'a', 'b', 0x00}, hugeArray.Bytes(), overlong}
for _, reported := range []string{"", "def456", strings.Repeat("ab", 21), strings.Repeat("zz", 20)} {
refused = append(refused, shaRecord(t, reported))
}
for _, data := range refused {
if _, err := DecodeRefRecord(data); err == nil {
t.Errorf("DecodeRefRecord accepted % x", data)
}
}
for _, reported := range []string{strings.Repeat("ab", 20), strings.Repeat("AB", 32)} {
if _, err := DecodeRefRecord(shaRecord(t, reported)); err != nil {
t.Errorf("DecodeRefRecord(%q): %v", reported, err)
}
}
}
func TestEncodeRefRecordRefusesWhatItsOwnDecoderWouldReject(t *testing.T) {
present := sha(t, strings.Repeat("ab", 20))
huge := strings.Repeat("x", cbg.MaxLength+1)
for _, tt := range []struct {
name string
rec RefRecord
}{
{"no sha at all", RefRecord{}},
{"editor past the cbor limit", RefRecord{Sha: present, Editor: syntax.DID(huge)}},
{"push option past the cbor limit", RefRecord{Sha: present, PushOptions: []string{huge}}},
} {
if _, err := encodeRefRecord(tt.rec); err == nil {
t.Errorf("%s encoded anyway, and the record would fail its own decode", tt.name)
}
}
}
func TestEscapeRefnameRulesTheRkey(t *testing.T) {
for _, tt := range []struct {
refname string
want syntax.RecordKey
wantOK bool
}{
{"refs/heads/main", "refs~2fheads~2fmain", true},
{"refs/heads/~x", "refs~2fheads~2f~7ex", true},
{strings.Repeat("/", 79), syntax.RecordKey(strings.Repeat("~2f", 79)), true},
{strings.Repeat("/", 80), "", false},
} {
rkey, ok := EscapeRefname(tt.refname)
if ok != tt.wantOK || (ok && rkey != tt.want) {
t.Fatalf("EscapeRefname(%q) = %q, %v", tt.refname, rkey, ok)
}
}
}
func TestUnescapeRkeyRoundTripsPlainAndEscapedRefnames(t *testing.T) {
for _, refname := range []string{
"refs/heads/main",
"main",
"refs/tags/v1.0.0+build",
"refs/heads/feature/foo_bar-squid.conch",
"refs/heads/~tilde",
"refs/heads/ branch with spaces ",
"refs/heads/日本語ブランチ",
"refs/heads/emoji-🍀",
} {
rkey, ok := EscapeRefname(refname)
if !ok {
t.Fatalf("EscapeRefname refused %q", refname)
}
decoded, ok := UnescapeRkey(rkey)
if !ok {
t.Fatalf("UnescapeRkey refused %q", rkey)
}
if decoded != refname {
t.Fatalf("round trip of %q answered %q", refname, decoded)
}
}
}
func TestUnescapeRkeyRefusesMalformedKeys(t *testing.T) {
for _, rkey := range []syntax.RecordKey{
"refs~2fheads~",
"refs~2fheads~2",
"refs~2fheads~zz",
"refs~2fhea ds",
"refs~ff",
} {
if decoded, ok := UnescapeRkey(rkey); ok {
t.Fatalf("UnescapeRkey(%q) = %q, want refusal", rkey, decoded)
}
}
}