Files
2026-07-24 12:59:48 -07:00

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")
}
}