mirror of
https://github.com/luxfi/ids.git
synced 2026-10-30 16:22:36 +00:00
UnmarshalText previously delegated to UnmarshalJSON, which required quoted input. Go's encoding/json decodes map keys via the TextUnmarshaler contract, which passes the UNQUOTED key. This made json.Unmarshal of any map[ids.ID]V fail with errMissingQuotes — visible to operators as luxd rejecting --chain-aliases-file and --chain-aliases-file-content with 'first and last characters should be quotes' even on well-formed input. The fix is symmetry: MarshalText emits id.String() unquoted, so UnmarshalText now parses the unquoted CB58/native string directly (matching FromString). Quoted input continues to round-trip through UnmarshalJSON, untouched. Regression test covers both the unquoted text path and the actual map[ID][]string json round-trip that motivated the fix.
385 lines
8.0 KiB
Go
385 lines
8.0 KiB
Go
// Copyright (C) 2020-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package ids
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"slices"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/luxfi/crypto/cb58"
|
|
"github.com/luxfi/crypto/hash"
|
|
)
|
|
|
|
func TestID(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
id := ID{24}
|
|
idCopy := ID{24}
|
|
prefixed := id.Prefix(0)
|
|
|
|
require.Equal(idCopy, id)
|
|
require.Equal(prefixed, id.Prefix(0))
|
|
}
|
|
|
|
func TestIDPrefix(t *testing.T) {
|
|
id := GenerateTestID()
|
|
tests := []struct {
|
|
name string
|
|
id ID
|
|
prefix []uint64
|
|
expectedPreimage []byte
|
|
}{
|
|
{
|
|
name: "empty prefix",
|
|
id: id,
|
|
prefix: []uint64{},
|
|
expectedPreimage: id[:],
|
|
},
|
|
{
|
|
name: "1 prefix",
|
|
id: id,
|
|
prefix: []uint64{1},
|
|
expectedPreimage: slices.Concat(
|
|
[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01},
|
|
id[:],
|
|
),
|
|
},
|
|
{
|
|
name: "multiple prefixes",
|
|
id: id,
|
|
prefix: []uint64{1, 256},
|
|
expectedPreimage: slices.Concat(
|
|
[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01},
|
|
[]byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00},
|
|
id[:],
|
|
),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
expected := ID(hash.ComputeHash256Array(test.expectedPreimage))
|
|
require.Equal(t, expected, test.id.Prefix(test.prefix...))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIDAppend(t *testing.T) {
|
|
id := GenerateTestID()
|
|
tests := []struct {
|
|
name string
|
|
id ID
|
|
suffix []uint32
|
|
expectedPreimage []byte
|
|
}{
|
|
{
|
|
name: "empty suffix",
|
|
id: id,
|
|
suffix: []uint32{},
|
|
expectedPreimage: id[:],
|
|
},
|
|
{
|
|
name: "1 suffix",
|
|
id: id,
|
|
suffix: []uint32{1},
|
|
expectedPreimage: slices.Concat(
|
|
id[:],
|
|
[]byte{0x00, 0x00, 0x00, 0x01},
|
|
),
|
|
},
|
|
{
|
|
name: "multiple suffixes",
|
|
id: id,
|
|
suffix: []uint32{1, 256},
|
|
expectedPreimage: slices.Concat(
|
|
id[:],
|
|
[]byte{0x00, 0x00, 0x00, 0x01},
|
|
[]byte{0x00, 0x00, 0x01, 0x00},
|
|
),
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
expected := ID(hash.ComputeHash256Array(test.expectedPreimage))
|
|
require.Equal(t, expected, test.id.Append(test.suffix...))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIDXOR(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
id1 := ID{1}
|
|
id3 := ID{3}
|
|
|
|
require.Equal(ID{2}, id1.XOR(id3))
|
|
require.Equal(ID{1}, id1)
|
|
}
|
|
|
|
func TestIDBit(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
id0 := ID{1 << 0}
|
|
id1 := ID{1 << 1}
|
|
id2 := ID{1 << 2}
|
|
id3 := ID{1 << 3}
|
|
id4 := ID{1 << 4}
|
|
id5 := ID{1 << 5}
|
|
id6 := ID{1 << 6}
|
|
id7 := ID{1 << 7}
|
|
id8 := ID{0, 1 << 0}
|
|
|
|
require.Equal(1, id0.Bit(0))
|
|
require.Equal(1, id1.Bit(1))
|
|
require.Equal(1, id2.Bit(2))
|
|
require.Equal(1, id3.Bit(3))
|
|
require.Equal(1, id4.Bit(4))
|
|
require.Equal(1, id5.Bit(5))
|
|
require.Equal(1, id6.Bit(6))
|
|
require.Equal(1, id7.Bit(7))
|
|
require.Equal(1, id8.Bit(8))
|
|
}
|
|
|
|
func TestFromString(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
id := ID{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'}
|
|
idStr := id.String()
|
|
id2, err := FromString(idStr)
|
|
require.NoError(err)
|
|
require.Equal(id, id2)
|
|
}
|
|
|
|
func TestIDFromStringError(t *testing.T) {
|
|
tests := []struct {
|
|
in string
|
|
expectedErr error
|
|
}{
|
|
{
|
|
in: "",
|
|
expectedErr: cb58.ErrBase58Decoding,
|
|
},
|
|
{
|
|
in: "foo",
|
|
expectedErr: cb58.ErrMissingChecksum,
|
|
},
|
|
{
|
|
in: "foobar",
|
|
expectedErr: cb58.ErrBadChecksum,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.in, func(t *testing.T) {
|
|
_, err := FromString(tt.in)
|
|
require.ErrorIs(t, err, tt.expectedErr)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIDMarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
label string
|
|
in ID
|
|
out []byte
|
|
err error
|
|
}{
|
|
{
|
|
"ID{}",
|
|
ID{},
|
|
[]byte(`"11111111111111111111111111111111LpoYY"`),
|
|
nil,
|
|
},
|
|
{
|
|
`ID("ava labs")`,
|
|
ID{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'},
|
|
[]byte(`"jvYi6Tn9idMi7BaymUVi9zWjg5tpmW7trfKG1AYJLKZJ2fsU7"`),
|
|
nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.label, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
out, err := tt.in.MarshalJSON()
|
|
require.ErrorIs(err, tt.err)
|
|
require.Equal(tt.out, out)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIDUnmarshalJSON(t *testing.T) {
|
|
tests := []struct {
|
|
label string
|
|
in []byte
|
|
out ID
|
|
err error
|
|
}{
|
|
{
|
|
"ID{}",
|
|
[]byte("null"),
|
|
ID{},
|
|
nil,
|
|
},
|
|
{
|
|
`ID("ava labs")`,
|
|
[]byte(`"jvYi6Tn9idMi7BaymUVi9zWjg5tpmW7trfKG1AYJLKZJ2fsU7"`),
|
|
ID{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'},
|
|
nil,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.label, func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
foo := ID{}
|
|
err := foo.UnmarshalJSON(tt.in)
|
|
require.ErrorIs(err, tt.err)
|
|
require.Equal(tt.out, foo)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestIDHex(t *testing.T) {
|
|
id := ID{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'}
|
|
expected := "617661206c616273000000000000000000000000000000000000000000000000"
|
|
require.Equal(t, expected, id.Hex())
|
|
}
|
|
|
|
// TestIDUnmarshalText covers the unquoted CB58 path that
|
|
// encoding/json invokes for map keys. Regression: the previous
|
|
// implementation delegated to UnmarshalJSON and rejected unquoted input
|
|
// with errMissingQuotes, so json.Unmarshal of any map[ids.ID]V failed
|
|
// even for byte-identical content that worked in struct fields.
|
|
func TestIDUnmarshalText(t *testing.T) {
|
|
tests := []struct {
|
|
label string
|
|
in string
|
|
out ID
|
|
}{
|
|
{
|
|
"empty -> Empty",
|
|
"",
|
|
Empty,
|
|
},
|
|
{
|
|
"null -> Empty",
|
|
"null",
|
|
Empty,
|
|
},
|
|
{
|
|
"unquoted CB58",
|
|
"jvYi6Tn9idMi7BaymUVi9zWjg5tpmW7trfKG1AYJLKZJ2fsU7",
|
|
ID{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.label, func(t *testing.T) {
|
|
require := require.New(t)
|
|
var got ID
|
|
require.NoError(got.UnmarshalText([]byte(tt.in)))
|
|
require.Equal(tt.out, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestIDMapKeyJSONRoundTrip is the actual production scenario:
|
|
// map[ids.ID][]string used by luxd's chain-aliases parser. Both
|
|
// directions must succeed against the same Go encoding/json runtime.
|
|
func TestIDMapKeyJSONRoundTrip(t *testing.T) {
|
|
require := require.New(t)
|
|
id := ID{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'}
|
|
in := map[ID][]string{id: {"hanzo", "h"}}
|
|
|
|
enc, err := json.Marshal(in)
|
|
require.NoError(err)
|
|
|
|
var out map[ID][]string
|
|
require.NoError(json.Unmarshal(enc, &out))
|
|
require.Equal(in, out)
|
|
}
|
|
|
|
func TestIDString(t *testing.T) {
|
|
tests := []struct {
|
|
label string
|
|
id ID
|
|
expected string
|
|
}{
|
|
{"ID{}", ID{}, "11111111111111111111111111111111LpoYY"},
|
|
{"ID{24}", ID{24}, "Ba3mm8Ra8JYYebeZ9p7zw1ayorDbeD1euwxhgzSLsncKqGoNt"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.label, func(t *testing.T) {
|
|
require.Equal(t, tt.expected, tt.id.String())
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSortIDs(t *testing.T) {
|
|
ids := []ID{
|
|
{'e', 'v', 'a', ' ', 'l', 'a', 'b', 's'},
|
|
{'W', 'a', 'l', 'l', 'e', ' ', 'l', 'a', 'b', 's'},
|
|
{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'},
|
|
}
|
|
slices.SortFunc(ids, func(a, b ID) int {
|
|
return a.Compare(b)
|
|
})
|
|
expected := []ID{
|
|
{'W', 'a', 'l', 'l', 'e', ' ', 'l', 'a', 'b', 's'},
|
|
{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'},
|
|
{'e', 'v', 'a', ' ', 'l', 'a', 'b', 's'},
|
|
}
|
|
require.Equal(t, expected, ids)
|
|
}
|
|
|
|
func TestIDMapMarshalling(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
originalMap := map[ID]int{
|
|
{'e', 'v', 'a', ' ', 'l', 'a', 'b', 's'}: 1,
|
|
{'a', 'v', 'a', ' ', 'l', 'a', 'b', 's'}: 2,
|
|
}
|
|
mapJSON, err := json.Marshal(originalMap)
|
|
require.NoError(err)
|
|
|
|
var unmarshalledMap map[ID]int
|
|
require.NoError(json.Unmarshal(mapJSON, &unmarshalledMap))
|
|
|
|
require.Equal(originalMap, unmarshalledMap)
|
|
}
|
|
|
|
func TestIDCompare(t *testing.T) {
|
|
tests := []struct {
|
|
a ID
|
|
b ID
|
|
expected int
|
|
}{
|
|
{
|
|
a: ID{1},
|
|
b: ID{0},
|
|
expected: 1,
|
|
},
|
|
{
|
|
a: ID{1},
|
|
b: ID{1},
|
|
expected: 0,
|
|
},
|
|
{
|
|
a: ID{1, 0},
|
|
b: ID{1, 2},
|
|
expected: -1,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(fmt.Sprintf("%s_%s_%d", test.a, test.b, test.expected), func(t *testing.T) {
|
|
require := require.New(t)
|
|
|
|
require.Equal(test.expected, test.a.Compare(test.b))
|
|
require.Equal(-test.expected, test.b.Compare(test.a))
|
|
})
|
|
}
|
|
}
|