go-p2p/node/levin/storage_test.go
Claude 101ef37985
feat(levin): portable storage section encode/decode
Co-Authored-By: Charon <charon@lethean.io>
2026-02-20 19:27:31 +00:00

337 lines
8 KiB
Go

// Copyright (c) 2024-2026 Lethean Contributors
// SPDX-License-Identifier: EUPL-1.2
package levin
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEncodeStorage_EmptySection(t *testing.T) {
s := Section{}
data, err := EncodeStorage(s)
require.NoError(t, err)
// 9-byte header + 1-byte varint(0) = 10 bytes.
assert.Len(t, data, 10)
// Verify storage header signatures.
assert.Equal(t, byte(0x01), data[0])
assert.Equal(t, byte(0x11), data[1])
assert.Equal(t, byte(0x01), data[2])
assert.Equal(t, byte(0x01), data[3])
assert.Equal(t, byte(0x01), data[4])
assert.Equal(t, byte(0x01), data[5])
assert.Equal(t, byte(0x02), data[6])
assert.Equal(t, byte(0x01), data[7])
// Version byte.
assert.Equal(t, byte(1), data[8])
// Entry count varint: 0.
assert.Equal(t, byte(0x00), data[9])
}
func TestStorage_PrimitivesRoundTrip(t *testing.T) {
s := Section{
"u64": Uint64Val(0xDEADBEEFCAFEBABE),
"u32": Uint32Val(0xCAFEBABE),
"u16": Uint16Val(0xBEEF),
"u8": Uint8Val(42),
"i64": Int64Val(-9223372036854775808),
"i32": Int32Val(-2147483648),
"i16": Int16Val(-32768),
"i8": Int8Val(-128),
"flag": BoolVal(true),
"height": StringVal([]byte("hello world")),
"pi": DoubleVal(3.141592653589793),
}
data, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
// Unsigned integers.
u64, err := decoded["u64"].AsUint64()
require.NoError(t, err)
assert.Equal(t, uint64(0xDEADBEEFCAFEBABE), u64)
u32, err := decoded["u32"].AsUint32()
require.NoError(t, err)
assert.Equal(t, uint32(0xCAFEBABE), u32)
u16, err := decoded["u16"].AsUint16()
require.NoError(t, err)
assert.Equal(t, uint16(0xBEEF), u16)
u8, err := decoded["u8"].AsUint8()
require.NoError(t, err)
assert.Equal(t, uint8(42), u8)
// Signed integers.
i64, err := decoded["i64"].AsInt64()
require.NoError(t, err)
assert.Equal(t, int64(-9223372036854775808), i64)
i32, err := decoded["i32"].AsInt32()
require.NoError(t, err)
assert.Equal(t, int32(-2147483648), i32)
i16, err := decoded["i16"].AsInt16()
require.NoError(t, err)
assert.Equal(t, int16(-32768), i16)
i8, err := decoded["i8"].AsInt8()
require.NoError(t, err)
assert.Equal(t, int8(-128), i8)
// Bool.
flag, err := decoded["flag"].AsBool()
require.NoError(t, err)
assert.True(t, flag)
// String.
str, err := decoded["height"].AsString()
require.NoError(t, err)
assert.Equal(t, []byte("hello world"), str)
// Double.
pi, err := decoded["pi"].AsDouble()
require.NoError(t, err)
assert.Equal(t, 3.141592653589793, pi)
}
func TestStorage_NestedObject(t *testing.T) {
inner := Section{
"port": Uint16Val(18080),
"host": StringVal([]byte("127.0.0.1")),
}
outer := Section{
"node_data": ObjectVal(inner),
"version": Uint32Val(1),
}
data, err := EncodeStorage(outer)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
ver, err := decoded["version"].AsUint32()
require.NoError(t, err)
assert.Equal(t, uint32(1), ver)
innerDec, err := decoded["node_data"].AsSection()
require.NoError(t, err)
port, err := innerDec["port"].AsUint16()
require.NoError(t, err)
assert.Equal(t, uint16(18080), port)
host, err := innerDec["host"].AsString()
require.NoError(t, err)
assert.Equal(t, []byte("127.0.0.1"), host)
}
func TestStorage_Uint64Array(t *testing.T) {
s := Section{
"heights": Uint64ArrayVal([]uint64{10, 20, 30}),
}
data, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
arr, err := decoded["heights"].AsUint64Array()
require.NoError(t, err)
assert.Equal(t, []uint64{10, 20, 30}, arr)
}
func TestStorage_StringArray(t *testing.T) {
s := Section{
"peers": StringArrayVal([][]byte{[]byte("foo"), []byte("bar")}),
}
data, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
arr, err := decoded["peers"].AsStringArray()
require.NoError(t, err)
require.Len(t, arr, 2)
assert.Equal(t, []byte("foo"), arr[0])
assert.Equal(t, []byte("bar"), arr[1])
}
func TestStorage_ObjectArray(t *testing.T) {
sections := []Section{
{"id": Uint32Val(1), "name": StringVal([]byte("alice"))},
{"id": Uint32Val(2), "name": StringVal([]byte("bob"))},
}
s := Section{
"nodes": ObjectArrayVal(sections),
}
data, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
arr, err := decoded["nodes"].AsSectionArray()
require.NoError(t, err)
require.Len(t, arr, 2)
id1, err := arr[0]["id"].AsUint32()
require.NoError(t, err)
assert.Equal(t, uint32(1), id1)
name1, err := arr[0]["name"].AsString()
require.NoError(t, err)
assert.Equal(t, []byte("alice"), name1)
id2, err := arr[1]["id"].AsUint32()
require.NoError(t, err)
assert.Equal(t, uint32(2), id2)
name2, err := arr[1]["name"].AsString()
require.NoError(t, err)
assert.Equal(t, []byte("bob"), name2)
}
func TestDecodeStorage_BadSignature(t *testing.T) {
// Corrupt the first 4 bytes.
data := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0x01, 0x01, 0x02, 0x01, 0x01, 0x00}
_, err := DecodeStorage(data)
require.Error(t, err)
assert.ErrorIs(t, err, ErrStorageBadSignature)
}
func TestDecodeStorage_TooShort(t *testing.T) {
_, err := DecodeStorage([]byte{0x01, 0x11})
require.Error(t, err)
assert.ErrorIs(t, err, ErrStorageTruncated)
}
func TestStorage_ByteIdenticalReencode(t *testing.T) {
s := Section{
"alpha": Uint64Val(999),
"bravo": StringVal([]byte("deterministic")),
"charlie": BoolVal(false),
"delta": ObjectVal(Section{
"x": Int32Val(-42),
"y": Int32Val(100),
}),
"echo": Uint64ArrayVal([]uint64{1, 2, 3}),
}
data1, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data1)
require.NoError(t, err)
data2, err := EncodeStorage(decoded)
require.NoError(t, err)
assert.Equal(t, data1, data2, "re-encoded bytes must be identical")
}
func TestStorage_TypeMismatchErrors(t *testing.T) {
v := Uint64Val(42)
_, err := v.AsUint32()
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
_, err = v.AsString()
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
_, err = v.AsBool()
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
_, err = v.AsSection()
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
_, err = v.AsUint64Array()
assert.ErrorIs(t, err, ErrStorageTypeMismatch)
}
func TestStorage_Uint32Array(t *testing.T) {
s := Section{
"ports": Uint32ArrayVal([]uint32{8080, 8443, 9090}),
}
data, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
arr, err := decoded["ports"].AsUint32Array()
require.NoError(t, err)
assert.Equal(t, []uint32{8080, 8443, 9090}, arr)
}
func TestDecodeStorage_BadVersion(t *testing.T) {
// Valid signatures but version 2 instead of 1.
data := []byte{0x01, 0x11, 0x01, 0x01, 0x01, 0x01, 0x02, 0x01, 0x02, 0x00}
_, err := DecodeStorage(data)
require.Error(t, err)
assert.ErrorIs(t, err, ErrStorageBadVersion)
}
func TestStorage_EmptyArrays(t *testing.T) {
s := Section{
"empty_u64": Uint64ArrayVal([]uint64{}),
"empty_str": StringArrayVal([][]byte{}),
"empty_obj": ObjectArrayVal([]Section{}),
}
data, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
u64arr, err := decoded["empty_u64"].AsUint64Array()
require.NoError(t, err)
assert.Empty(t, u64arr)
strarr, err := decoded["empty_str"].AsStringArray()
require.NoError(t, err)
assert.Empty(t, strarr)
objarr, err := decoded["empty_obj"].AsSectionArray()
require.NoError(t, err)
assert.Empty(t, objarr)
}
func TestStorage_BoolFalseRoundTrip(t *testing.T) {
s := Section{
"off": BoolVal(false),
"on": BoolVal(true),
}
data, err := EncodeStorage(s)
require.NoError(t, err)
decoded, err := DecodeStorage(data)
require.NoError(t, err)
off, err := decoded["off"].AsBool()
require.NoError(t, err)
assert.False(t, off)
on, err := decoded["on"].AsBool()
require.NoError(t, err)
assert.True(t, on)
}