mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-09-01 16:48:19 +00:00
257 lines
6.4 KiB
Go
257 lines
6.4 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package config
|
|
|
|
import (
|
|
"context"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
type stubSeeder struct {
|
|
regions map[string]int32
|
|
iatas []string
|
|
regionIATAs map[int32][]string
|
|
scopes []string
|
|
borders map[string]json.RawMessage
|
|
upsertErr error
|
|
}
|
|
|
|
func newStubSeeder() *stubSeeder {
|
|
return &stubSeeder{
|
|
regions: make(map[string]int32),
|
|
regionIATAs: make(map[int32][]string),
|
|
borders: make(map[string]json.RawMessage),
|
|
}
|
|
}
|
|
|
|
func (s *stubSeeder) UpsertIATA(_ context.Context, iata string) error {
|
|
s.iatas = append(s.iatas, iata)
|
|
return s.upsertErr
|
|
}
|
|
|
|
func (s *stubSeeder) UpsertIATADetails(_ context.Context, iata, _ string, _, _ *float64) error {
|
|
s.iatas = append(s.iatas, iata)
|
|
return s.upsertErr
|
|
}
|
|
|
|
func (s *stubSeeder) UpsertIATABorder(_ context.Context, iata string, border json.RawMessage) error {
|
|
s.borders[iata] = border
|
|
return s.upsertErr
|
|
}
|
|
|
|
func (s *stubSeeder) UpsertRegion(_ context.Context, slug, _, _ string, _ int, _, _ *float64, _ *int) (int32, error) {
|
|
id := int32(len(s.regions) + 1)
|
|
s.regions[slug] = id
|
|
return id, s.upsertErr
|
|
}
|
|
|
|
func (s *stubSeeder) UpsertRegionIATA(_ context.Context, regionID int32, iata string) error {
|
|
s.regionIATAs[regionID] = append(s.regionIATAs[regionID], iata)
|
|
return s.upsertErr
|
|
}
|
|
|
|
func (s *stubSeeder) UpsertTransportScope(_ context.Context, name, _ string, _, _ []byte) error {
|
|
s.scopes = append(s.scopes, name)
|
|
return s.upsertErr
|
|
}
|
|
|
|
func TestSeed_IATAsAndRegions(t *testing.T) {
|
|
cfg := &Config{
|
|
IATAs: map[string]IATAConfig{
|
|
"YVR": {Name: "Vancouver"},
|
|
},
|
|
Regions: []RegionConfig{
|
|
{Slug: "bc", Name: "British Columbia", IATAs: []string{"YVR", "YYJ"}},
|
|
},
|
|
}
|
|
|
|
db := newStubSeeder()
|
|
if err := Seed(context.Background(), cfg, db); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if _, ok := db.regions["bc"]; !ok {
|
|
t.Error("expected bc region to be upserted")
|
|
}
|
|
if len(db.regionIATAs[1]) != 2 {
|
|
t.Errorf("expected 2 IATAs for region 1, got %d", len(db.regionIATAs[1]))
|
|
}
|
|
}
|
|
|
|
func TestSeed_BorderFile_ValidatesAndUpserts(t *testing.T) {
|
|
dir := t.TempDir()
|
|
borderPath := dir + "/yow.geojson"
|
|
if err := os.WriteFile(borderPath, []byte(`{
|
|
"type": "Feature",
|
|
"properties": {"name": "Ottawa"},
|
|
"geometry": {
|
|
"type": "Polygon",
|
|
"coordinates": [[[-76.4,45.0],[-75.2,45.0],[-75.2,45.6],[-76.4,45.6],[-76.4,45.0]]]
|
|
}
|
|
}`), 0o644); err != nil {
|
|
t.Fatalf("failed to write test border file: %v", err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
IATAs: map[string]IATAConfig{
|
|
"YOW": {Name: "Ottawa", BorderFile: borderPath},
|
|
},
|
|
}
|
|
db := newStubSeeder()
|
|
if err := Seed(context.Background(), cfg, db); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
border, ok := db.borders["YOW"]
|
|
if !ok {
|
|
t.Fatal("expected a border to be upserted for YOW")
|
|
}
|
|
var feat map[string]any
|
|
if err := json.Unmarshal(border, &feat); err != nil {
|
|
t.Fatalf("stored border is not valid JSON: %v", err)
|
|
}
|
|
if _, ok := feat["bbox"]; !ok {
|
|
t.Error("expected stored border to have a computed bbox")
|
|
}
|
|
}
|
|
|
|
func TestSeed_BorderFile_InvalidGeometryFailsClosed(t *testing.T) {
|
|
dir := t.TempDir()
|
|
borderPath := dir + "/bad.geojson"
|
|
// unclosed ring
|
|
if err := os.WriteFile(borderPath, []byte(`{
|
|
"type": "Feature",
|
|
"properties": {},
|
|
"geometry": {
|
|
"type": "Polygon",
|
|
"coordinates": [[[-76.4,45.0],[-75.2,45.0],[-75.2,45.6]]]
|
|
}
|
|
}`), 0o644); err != nil {
|
|
t.Fatalf("failed to write test border file: %v", err)
|
|
}
|
|
|
|
cfg := &Config{
|
|
IATAs: map[string]IATAConfig{
|
|
"YOW": {Name: "Ottawa", BorderFile: borderPath},
|
|
},
|
|
}
|
|
db := newStubSeeder()
|
|
if err := Seed(context.Background(), cfg, db); err == nil {
|
|
t.Fatal("expected Seed to fail on an invalid border file")
|
|
}
|
|
if _, ok := db.borders["YOW"]; ok {
|
|
t.Error("expected no border to be upserted when validation fails")
|
|
}
|
|
}
|
|
|
|
func TestSeed_BorderFile_MissingFileFailsClosed(t *testing.T) {
|
|
cfg := &Config{
|
|
IATAs: map[string]IATAConfig{
|
|
"YOW": {Name: "Ottawa", BorderFile: "/nonexistent/path/border.geojson"},
|
|
},
|
|
}
|
|
db := newStubSeeder()
|
|
if err := Seed(context.Background(), cfg, db); err == nil {
|
|
t.Fatal("expected Seed to fail when the border file doesn't exist")
|
|
}
|
|
}
|
|
|
|
func TestSeed_Scopes(t *testing.T) {
|
|
cfg := &Config{
|
|
Scopes: []ScopeConfig{
|
|
{Name: "bc"},
|
|
{Name: "#west"},
|
|
},
|
|
}
|
|
|
|
db := newStubSeeder()
|
|
if err := Seed(context.Background(), cfg, db); err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(db.scopes) != 2 {
|
|
t.Fatalf("expected 2 scopes, got %d", len(db.scopes))
|
|
}
|
|
if db.scopes[0] != "#bc" {
|
|
t.Errorf("expected #bc, got %s", db.scopes[0])
|
|
}
|
|
if db.scopes[1] != "#west" {
|
|
t.Errorf("expected #west, got %s", db.scopes[1])
|
|
}
|
|
}
|
|
|
|
func TestSeed_DBError(t *testing.T) {
|
|
cfg := &Config{
|
|
IATAs: map[string]IATAConfig{
|
|
"YVR": {Name: "Vancouver"},
|
|
},
|
|
}
|
|
|
|
db := newStubSeeder()
|
|
db.upsertErr = errors.New("db error")
|
|
|
|
if err := Seed(context.Background(), cfg, db); err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeScopeName_WithHash(t *testing.T) {
|
|
if normalizeScopeName("#bc") != "#bc" {
|
|
t.Error("expected #bc unchanged")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeScopeName_WithDollar(t *testing.T) {
|
|
if normalizeScopeName("$bc") != "$bc" {
|
|
t.Error("expected $bc unchanged")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeScopeName_WithoutPrefix(t *testing.T) {
|
|
if normalizeScopeName("bc") != "#bc" {
|
|
t.Error("expected bc to become #bc")
|
|
}
|
|
}
|
|
|
|
func TestNormalizeScopeName_Empty(t *testing.T) {
|
|
if normalizeScopeName("") != "#" {
|
|
t.Error("expected empty string to become #")
|
|
}
|
|
}
|
|
|
|
func TestDeriveScopeKey_Length(t *testing.T) {
|
|
key := deriveScopeKey("#bc")
|
|
if len(key) != 16 {
|
|
t.Errorf("expected 16 bytes, got %d", len(key))
|
|
}
|
|
}
|
|
|
|
func TestDeriveScopeKey_Deterministic(t *testing.T) {
|
|
a := deriveScopeKey("#bc")
|
|
b := deriveScopeKey("#bc")
|
|
if hex.EncodeToString(a) != hex.EncodeToString(b) {
|
|
t.Error("expected same key for same input")
|
|
}
|
|
}
|
|
|
|
func TestDeriveScopeKey_KnownValue(t *testing.T) {
|
|
// SHA256("#bc")[:16] — pin the exact derivation so changes are caught
|
|
key := deriveScopeKey("#bc")
|
|
got := hex.EncodeToString(key)
|
|
// generate this once: echo -n "#bc" | sha256sum | cut -c1-32
|
|
const want = "84509cfe73d94f7f6a8299e6bcdb8a3c"
|
|
if got != want {
|
|
t.Errorf("deriveScopeKey(\"#bc\") = %s, want %s", got, want)
|
|
}
|
|
}
|
|
|
|
func TestDeriveScopeKey_DifferentInputs(t *testing.T) {
|
|
a := deriveScopeKey("#bc")
|
|
b := deriveScopeKey("#other")
|
|
if hex.EncodeToString(a) == hex.EncodeToString(b) {
|
|
t.Error("expected different keys for different inputs")
|
|
}
|
|
}
|