mirror of
https://github.com/luxfi/api.git
synced 2026-09-24 00:24:53 +00:00
A ZAP field is an offset. Eight of these replies held a map, which has no order to give one, so the reply could not cross between two processes at all. Two more held values whose fields are every one unexported — netip.AddrPort, time.Time — and those are worse: they are not refused, they derive an EMPTY layout, so the reply crossed, reported no error and carried nothing. GetNodeIPReply has one field, so its whole answer was blank. And a health check says what it likes, so its details were an `any`, which has no shape until it holds something. Each map is now a list of a stated element type, ordered by the key it was an object of. An address is the text netip already wrote for it, an instant is three numbers, and a check details are the bytes of their own JSON — the one shape an object, a string and a list all share. The JSON does not move. Every list marshals to the object it replaced and reads one back, and wire_test.go holds that against the old shape spelled out as a literal rather than against itself. The order is new where there was none: a map answered the same question two ways. APIReply also states the status it carries, so the 503 a probe reads and the healthy a person reads are one fact computed once.
192 lines
6.2 KiB
Go
192 lines
6.2 KiB
Go
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
// The shapes that changed to cross a wire must not have changed the wire.
|
|
//
|
|
// A map became a list and an `any` became bytes so that a reply could cross
|
|
// between two processes at all — a ZAP field is an offset, and a map has no
|
|
// order to give one while an interface has no shape until it holds something.
|
|
// None of that is a caller's business: these APIs are answered by a live network
|
|
// today, so the JSON a caller reads must be the same bytes it always was.
|
|
//
|
|
// Each test below writes the NEW type and compares against the object the OLD
|
|
// one produced, spelled out as a literal. A literal rather than a round trip,
|
|
// because a round trip through one type proves only that it agrees with itself.
|
|
package api_test
|
|
|
|
import (
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/luxfi/api/admin"
|
|
"github.com/luxfi/api/health"
|
|
"github.com/luxfi/api/info"
|
|
"github.com/luxfi/api/types"
|
|
"github.com/luxfi/ids"
|
|
)
|
|
|
|
func id(b byte) ids.ID { return ids.ID{0: b} }
|
|
|
|
// same asserts v marshals to want, and that reading want back yields v — so the
|
|
// list is not merely writing the old shape but reading it too.
|
|
func same[T any](t *testing.T, v T, want string, back func(T) bool) {
|
|
t.Helper()
|
|
got, err := json.Marshal(v)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
if norm(t, string(got)) != norm(t, want) {
|
|
t.Errorf("marshalled\n %s\nwant\n %s", got, want)
|
|
}
|
|
var read T
|
|
if err := json.Unmarshal([]byte(want), &read); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
if !back(read) {
|
|
t.Errorf("reading %s back did not reproduce the value", want)
|
|
}
|
|
}
|
|
|
|
func norm(t *testing.T, s string) string {
|
|
t.Helper()
|
|
var v any
|
|
if err := json.Unmarshal([]byte(s), &v); err != nil {
|
|
t.Fatalf("not JSON: %s", s)
|
|
}
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
func TestVMVersionsIsStillAnObject(t *testing.T) {
|
|
same(t,
|
|
info.VMVersions{{VM: "platformvm", Version: "v1.2.3"}, {VM: "xvm", Version: "v1.2.4"}},
|
|
`{"platformvm":"v1.2.3","xvm":"v1.2.4"}`,
|
|
func(v info.VMVersions) bool {
|
|
return len(v) == 2 && v[0].VM == "platformvm" && v[1].Version == "v1.2.4"
|
|
})
|
|
}
|
|
|
|
func TestVMAliasesAndFxNamesAreStillObjects(t *testing.T) {
|
|
same(t,
|
|
info.VMAliases{{VM: id(1), Aliases: []string{"platformvm"}}},
|
|
`{"`+id(1).String()+`":["platformvm"]}`,
|
|
func(v info.VMAliases) bool { return len(v) == 1 && v[0].VM == id(1) })
|
|
|
|
same(t,
|
|
info.FxNames{{Fx: id(2), Name: "secp256k1fx"}},
|
|
`{"`+id(2).String()+`":"secp256k1fx"}`,
|
|
func(v info.FxNames) bool { return len(v) == 1 && v[0].Name == "secp256k1fx" })
|
|
}
|
|
|
|
func TestLPsAreStillAnObjectKeyedByNumber(t *testing.T) {
|
|
same(t,
|
|
info.LPs{{Number: 23, LP: info.LP{AbstainWeight: 7}}},
|
|
`{"23":{"supportWeight":"0","supporters":null,"objectWeight":"0","objectors":null,"abstainWeight":"7"}}`,
|
|
func(v info.LPs) bool { return len(v) == 1 && v[0].Number == 23 && v[0].LP.AbstainWeight == 7 })
|
|
}
|
|
|
|
func TestLoggerLevelsAreStillAnObject(t *testing.T) {
|
|
same(t,
|
|
admin.LoggerLevels{{Logger: "C", Levels: admin.LogAndDisplayLevels{LogLevel: "DEBUG", DisplayLevel: "INFO"}}},
|
|
`{"C":{"logLevel":"DEBUG","displayLevel":"INFO"}}`,
|
|
func(v admin.LoggerLevels) bool { return len(v) == 1 && v[0].Levels.LogLevel == "DEBUG" })
|
|
}
|
|
|
|
func TestInstalledVMsAreStillAnObjectKeyedByID(t *testing.T) {
|
|
same(t,
|
|
admin.InstalledVMs{{ID: "vm1", Aliases: []string{"a"}}},
|
|
`{"vm1":{"id":"vm1","aliases":["a"]}}`,
|
|
func(v admin.InstalledVMs) bool { return len(v) == 1 && v[0].ID == "vm1" })
|
|
}
|
|
|
|
func TestLoadedAndFailedVMsAreStillObjects(t *testing.T) {
|
|
same(t,
|
|
admin.LoadedVMs{{VM: id(3), Aliases: []string{"a"}}},
|
|
`{"`+id(3).String()+`":["a"]}`,
|
|
func(v admin.LoadedVMs) bool { return len(v) == 1 && v[0].VM == id(3) })
|
|
|
|
same(t,
|
|
admin.FailedVMs{{VM: id(4), Error: "no plugin"}},
|
|
`{"`+id(4).String()+`":"no plugin"}`,
|
|
func(v admin.FailedVMs) bool { return len(v) == 1 && v[0].Error == "no plugin" })
|
|
}
|
|
|
|
func TestHealthChecksAreStillAnObject(t *testing.T) {
|
|
when := types.TimeOf(time.Unix(1753479996, 0).UTC())
|
|
said, err := types.RawOf(map[string]any{"availableDiskBytes": 12})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
same(t,
|
|
health.Checks{{Name: "diskspace", Result: health.Result{Details: said, Timestamp: when, Duration: 1234}}},
|
|
`{"diskspace":{"message":{"availableDiskBytes":12},"timestamp":"2025-07-25T21:46:36Z","duration":1234}}`,
|
|
func(v health.Checks) bool { return len(v) == 1 && v[0].Name == "diskspace" })
|
|
}
|
|
|
|
// The details of a check are whatever it said. All three shapes mainnet answers
|
|
// with survive as themselves — an `any` that became bytes lost nothing.
|
|
func TestRawKeepsEveryShapeACheckAnswersWith(t *testing.T) {
|
|
for _, want := range []string{
|
|
`{"availableDiskBytes":12}`,
|
|
`"node is not a validator"`,
|
|
`["P","X","C"]`,
|
|
`null`,
|
|
} {
|
|
var r types.Raw
|
|
if err := json.Unmarshal([]byte(want), &r); err != nil {
|
|
t.Fatalf("unmarshal %s: %v", want, err)
|
|
}
|
|
got, err := json.Marshal(r)
|
|
if err != nil {
|
|
t.Fatalf("marshal %s: %v", want, err)
|
|
}
|
|
if string(got) != want {
|
|
t.Errorf("Raw turned %s into %s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// An address is text on the JSON wire and always was — including the empty
|
|
// string an invalid one has always been written as.
|
|
func TestAddrIsTheTextNetipWrites(t *testing.T) {
|
|
for _, want := range []string{`"203.0.113.9:9651"`, `""`} {
|
|
var a types.Addr
|
|
if err := json.Unmarshal([]byte(want), &a); err != nil {
|
|
t.Fatalf("unmarshal %s: %v", want, err)
|
|
}
|
|
got, err := json.Marshal(a)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != want {
|
|
t.Errorf("Addr turned %s into %s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// An instant is the RFC 3339 string time.Time writes, and reads back to the
|
|
// same instant in the same zone.
|
|
func TestTimeIsTheStringTimeTimeWrites(t *testing.T) {
|
|
when := time.Date(2026, 8, 29, 12, 0, 0, 0, time.FixedZone("", 2*3600))
|
|
got, err := json.Marshal(types.TimeOf(when))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
direct, err := json.Marshal(when)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != string(direct) {
|
|
t.Errorf("Time wrote %s; time.Time writes %s", got, direct)
|
|
}
|
|
|
|
var read types.Time
|
|
if err := json.Unmarshal(direct, &read); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !read.Time().Equal(when) {
|
|
t.Errorf("read back %s; want %s", read.Time(), when)
|
|
}
|
|
}
|