Files
MrAlders0n 0e242574fb feat(meshmapper): import region boundaries from the Zones API
Imported boundaries override borderFile on the border map and for foreign marking, falling back to the manual border when MeshMapper has none.
2026-09-30 12:38:36 -04:00

125 lines
4.3 KiB
Go

// Copyright 2026 Beacon Contributors
// SPDX-License-Identifier: AGPL-3.0-or-later
package api_test
import (
"context"
"encoding/json"
"errors"
"strings"
"testing"
"time"
"github.com/MeshCore-Beacon/beacon-server/internal/api"
"github.com/MeshCore-Beacon/beacon-server/internal/borders"
"github.com/MeshCore-Beacon/beacon-server/internal/cache"
"github.com/alicebob/miniredis/v2"
"github.com/google/uuid"
"github.com/paulmach/orb"
)
type borderReader struct {
api.Reader
node *api.Node
page api.Page[api.NodeSummary]
getCalls, listCalls int
err error
}
func (r *borderReader) GetNode(context.Context, uuid.UUID) (*api.Node, error) {
r.getCalls++
return r.node, r.err
}
func (r *borderReader) ListNodes(_ context.Context, _ int16, _ []string, _, _ *bool, _ []byte, _, _, _ string, _ int64, _ int32, _ bool) (api.Page[api.NodeSummary], error) {
r.listCalls++
return r.page, r.err
}
func TestLocalBordersAfterCache(t *testing.T) {
ctx := context.Background()
local, err := borders.New([]orb.Polygon{{{{10, 10}, {20, 10}, {20, 20}, {10, 20}, {10, 10}}}})
if err != nil {
t.Fatal(err)
}
other, err := borders.New([]orb.Polygon{{{{30, 30}, {40, 30}, {40, 40}, {30, 40}, {30, 30}}}})
if err != nil {
t.Fatal(err)
}
node := &api.Node{NodeSummary: api.NodeSummary{ID: uuid.New(), NodeType: 2, Latitude: new(15.0), Longitude: new(15.0), IATAs: []api.NodeIATA{{IATA: "ZZZ", LastHeard: 1}}}}
base := &borderReader{node: node, page: api.Page[api.NodeSummary]{Items: []api.NodeSummary{node.NodeSummary}, HasMore: true, NextCursor: new(int64(123))}}
mr := miniredis.RunT(t)
client := cache.NewClient(mr.Addr(), "", 0)
t.Cleanup(func() { client.Close() })
cached := cache.NewCachedReader(base, client, cache.CacheTTLs{Nodes: time.Hour})
first := api.WithLocalBorders(cached, borders.NewLive(local))
second := api.WithLocalBorders(cached, borders.NewLive(other))
if api.WithLocalBorders(cached, nil) != cached {
t.Fatal("disabled decorator changed reader")
}
for _, tc := range []struct {
reader api.Reader
want bool
}{{first, false}, {second, true}} {
got, err := tc.reader.GetNode(ctx, node.ID)
if err != nil {
t.Fatal(err)
}
if got.PossiblyForeign == nil || *got.PossiblyForeign != tc.want || got.IATAs[0].IATA != "ZZZ" {
t.Fatalf("detail: %+v", got)
}
page, err := tc.reader.ListNodes(ctx, 2, []string{"ZZZ"}, nil, nil, nil, "", "", "", 0, 50, false)
if err != nil {
t.Fatal(err)
}
if !page.HasMore || *page.NextCursor != 123 || page.Items[0].PossiblyForeign == nil || *page.Items[0].PossiblyForeign != tc.want {
t.Fatalf("page: %+v", page)
}
data, _ := json.Marshal(got)
if !strings.Contains(string(data), `"possiblyForeign":`) {
t.Fatal("known false was omitted")
}
}
if base.getCalls != 1 || base.listCalls != 2 {
t.Fatalf("extra reads: get=%d list=%d", base.getCalls, base.listCalls)
}
if node.PossiblyForeign != nil || base.page.Items[0].PossiblyForeign != nil {
t.Fatal("shared reader data was mutated")
}
disabled, err := cached.GetNode(ctx, node.ID)
if err != nil {
t.Fatal(err)
}
data, _ := json.Marshal(disabled)
if strings.Contains(string(data), "possiblyForeign") {
t.Fatal("classification leaked into cached data")
}
// Existing ingest invalidation still supplies fresh coordinates to the wrapper.
base.node.Longitude = new(35.0)
cached.(*cache.CachedReader).InvalidateNode(ctx, node.ID)
updated, err := first.GetNode(ctx, node.ID)
if err != nil || updated.PossiblyForeign == nil || !*updated.PossiblyForeign || base.getCalls != 2 {
t.Fatal("updated coordinates not classified")
}
}
func TestLocalBordersPreserveReaderErrors(t *testing.T) {
local, err := borders.New([]orb.Polygon{{{{10, 10}, {20, 10}, {20, 20}, {10, 20}, {10, 10}}}})
if err != nil {
t.Fatal(err)
}
want := errors.New("reader unavailable")
base := &borderReader{err: want}
reader := api.WithLocalBorders(base, borders.NewLive(local))
if _, err := reader.GetNode(context.Background(), uuid.New()); !errors.Is(err, want) {
t.Fatal(err)
}
if _, err := reader.ListNodes(context.Background(), 0, nil, nil, nil, nil, "", "", "", 0, 1, false); !errors.Is(err, want) {
t.Fatal(err)
}
base.err = nil
if node, err := reader.GetNode(context.Background(), uuid.New()); node != nil || err != nil {
t.Fatal("nil result changed")
}
}