Files
Harmon 747eed63b0 Basemap Pusher: catalog and distribution gateway for existing tiles
A Go service that scans existing basemap tile directories and vector files,
catalogues them in SQLite, issues scoped access tokens, and serves tiles and
downloads to downstream systems. It deliberately does not generate or process
raster data: GIS software produces the tiles, this service catalogues,
authorises and distributes them.

Capabilities
- scans basemap tile directories (manifest-based or raw z/x/y) and vector
  assets (.geojson, .json, .zip, shapefiles with sidecars)
- token auth with per-resource grants, expiry and coarse scopes (read, slice)
- resource health inspection flags a moved or deleted tile root before a client
  hits a 404
- downstream push with exponential-backoff retries and manual re-queue
- image slicing jobs driven by the GDAL bundled with QGIS, including VRT
  mosaicking so successive drone flights accumulate into one seamless layer
- a narrow, non-admin slicing API for external callers: submit, poll,
  idempotent by job code, completion callback
- ops console covering health, ledger, tokens, pushes, scans, slicing and
  service info
- native Windows service install/start/stop with crash recovery, auto-start and
  a rotating log file, so no NSSM or WinSW wrapper is needed

Verified with 49 tests plus live runs against real tile data and real GDAL.
2026-10-16 10:36:12 +08:00

398 lines
12 KiB
Go

package basemap
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
)
func openTestStore(t *testing.T, cfg StoreConfig) *Store {
t.Helper()
store, err := OpenStore(cfg)
if err != nil {
t.Fatalf("open store: %v", err)
}
t.Cleanup(func() {
if err := store.Close(); err != nil {
t.Fatalf("close store: %v", err)
}
})
return store
}
func TestRunScanRefusesConcurrentRuns(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
tileRoot := filepath.Join(root, "tiles")
writeTestFile(t, filepath.Join(tileRoot, "google_image", "0", "0", "0.webp"), "tile")
vectorRoot := filepath.Join(root, "vectors")
store := openTestStore(t, StoreConfig{
SQLitePath: filepath.Join(root, "basemap.db"),
ScanRoot: tileRoot,
VectorScanRoot: vectorRoot,
APIBaseURL: "http://127.0.0.1:8910",
TileBaseURL: "http://127.0.0.1:8910",
VectorBaseURL: "http://127.0.0.1:8910",
})
store.scanMu.Lock()
if _, err := store.RunScan(ctx, defaultScanSourceCode); !errors.Is(err, ErrScanInProgress) {
store.scanMu.Unlock()
t.Fatalf("expected ErrScanInProgress, got %v", err)
}
if _, err := store.RunVectorScan(ctx); !errors.Is(err, ErrScanInProgress) {
store.scanMu.Unlock()
t.Fatalf("expected ErrScanInProgress from vector scan, got %v", err)
}
store.scanMu.Unlock()
if _, err := store.RunScan(ctx, defaultScanSourceCode); err != nil {
t.Fatalf("run scan after unlock: %v", err)
}
}
func TestRunScanReportsUnknownSource(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
store := openTestStore(t, StoreConfig{SQLitePath: filepath.Join(root, "basemap.db")})
if _, err := store.RunScan(ctx, "does-not-exist"); !errors.Is(err, ErrScanSourceNotFound) {
t.Fatalf("expected ErrScanSourceNotFound, got %v", err)
}
}
func TestRescanOnlyCountsRealChanges(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
tileRoot := filepath.Join(root, "tiles")
writeTestFile(t, filepath.Join(tileRoot, "google_image", "0", "0", "0.webp"), "tile")
store := openTestStore(t, StoreConfig{
SQLitePath: filepath.Join(root, "basemap.db"),
ScanRoot: tileRoot,
APIBaseURL: "http://127.0.0.1:8910",
TileBaseURL: "http://127.0.0.1:8910",
VectorBaseURL: "http://127.0.0.1:8910",
})
first, err := store.RunScan(ctx, defaultScanSourceCode)
if err != nil {
t.Fatalf("first scan: %v", err)
}
if first.AddedCount != 1 {
t.Fatalf("expected 1 added on first scan, got %d", first.AddedCount)
}
second, err := store.RunScan(ctx, defaultScanSourceCode)
if err != nil {
t.Fatalf("second scan: %v", err)
}
if second.UpdatedCount != 0 {
t.Fatalf("expected 0 updates on an unchanged rescan, got %d", second.UpdatedCount)
}
if second.AddedCount != 0 || second.RemovedCount != 0 {
t.Fatalf("expected no additions/removals, got +%d/-%d", second.AddedCount, second.RemovedCount)
}
if got := second.Summary["unchanged_count"]; got != 1 {
t.Fatalf("expected unchanged_count=1, got %v", got)
}
// A new zoom level is a genuine change and must be reported as an update.
writeTestFile(t, filepath.Join(tileRoot, "google_image", "1", "0", "0.webp"), "tile")
third, err := store.RunScan(ctx, defaultScanSourceCode)
if err != nil {
t.Fatalf("third scan: %v", err)
}
if third.UpdatedCount != 1 {
t.Fatalf("expected 1 update after adding a zoom level, got %d", third.UpdatedCount)
}
}
func TestVectorCodeCollisionKeepsBothAssets(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
vectorRoot := filepath.Join(root, "vectors")
writeTestFile(t, filepath.Join(vectorRoot, "a", "b.geojson"), `{"type":"FeatureCollection","features":[]}`)
writeTestFile(t, filepath.Join(vectorRoot, "a-b.geojson"), `{"type":"FeatureCollection","features":[]}`)
store := openTestStore(t, StoreConfig{
SQLitePath: filepath.Join(root, "basemap.db"),
VectorScanRoot: vectorRoot,
APIBaseURL: "http://127.0.0.1:8910",
VectorBaseURL: "http://127.0.0.1:8910",
})
if _, err := store.RunVectorScan(ctx); err != nil {
t.Fatalf("first vector scan: %v", err)
}
assets, err := store.ListVectorAssets(ctx)
if err != nil {
t.Fatalf("list assets: %v", err)
}
if len(assets) != 2 {
t.Fatalf("expected both colliding assets to survive, got %d", len(assets))
}
if assets[0].Code == assets[1].Code {
t.Fatalf("expected distinct codes, both are %q", assets[0].Code)
}
firstCodes := []string{assets[0].Code, assets[1].Code}
if _, err := store.RunVectorScan(ctx); err != nil {
t.Fatalf("second vector scan: %v", err)
}
assets, err = store.ListVectorAssets(ctx)
if err != nil {
t.Fatalf("list assets after rescan: %v", err)
}
if len(assets) != 2 {
t.Fatalf("rescan dropped an asset, now %d", len(assets))
}
secondCodes := map[string]bool{assets[0].Code: true, assets[1].Code: true}
for _, code := range firstCodes {
if !secondCodes[code] {
t.Fatalf("disambiguated code %q was not stable across rescans", code)
}
}
}
func TestVectorScanReusesChecksumUntilFileChanges(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
vectorRoot := filepath.Join(root, "vectors")
file := filepath.Join(vectorRoot, "boundary.geojson")
writeTestFile(t, file, `{"type":"FeatureCollection","features":[]}`)
store := openTestStore(t, StoreConfig{
SQLitePath: filepath.Join(root, "basemap.db"),
VectorScanRoot: vectorRoot,
APIBaseURL: "http://127.0.0.1:8910",
VectorBaseURL: "http://127.0.0.1:8910",
})
if _, err := store.RunVectorScan(ctx); err != nil {
t.Fatalf("first scan: %v", err)
}
first, err := store.GetVectorAssetByCode(ctx, "boundary")
if err != nil {
t.Fatalf("get asset: %v", err)
}
if first.Metadata["hash_size"] == nil || first.Metadata["hash_mtime"] == nil {
t.Fatalf("expected hash stamp in metadata, got %v", first.Metadata)
}
second, err := store.RunVectorScan(ctx)
if err != nil {
t.Fatalf("second scan: %v", err)
}
if second["unchanged_count"] != 1 {
t.Fatalf("expected unchanged_count=1, got %v", second["unchanged_count"])
}
reused, err := store.GetVectorAssetByCode(ctx, "boundary")
if err != nil {
t.Fatalf("get asset after rescan: %v", err)
}
if reused.Checksum != first.Checksum {
t.Fatalf("checksum changed without a file change: %q -> %q", first.Checksum, reused.Checksum)
}
// Rewriting the file with different content must invalidate the cached hash.
writeTestFile(t, file, `{"type":"FeatureCollection","features":[{"type":"Feature"}]}`)
if _, err := store.RunVectorScan(ctx); err != nil {
t.Fatalf("third scan: %v", err)
}
changed, err := store.GetVectorAssetByCode(ctx, "boundary")
if err != nil {
t.Fatalf("get asset after change: %v", err)
}
if changed.Checksum == first.Checksum {
t.Fatal("expected checksum to change after the file changed")
}
}
func TestCreateTokenAppliesDefaultTTL(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
tileRoot := filepath.Join(root, "tiles")
writeTestFile(t, filepath.Join(tileRoot, "google_image", "0", "0", "0.webp"), "tile")
store := openTestStore(t, StoreConfig{
SQLitePath: filepath.Join(root, "basemap.db"),
ScanRoot: tileRoot,
APIBaseURL: "http://127.0.0.1:8910",
TileBaseURL: "http://127.0.0.1:8910",
VectorBaseURL: "http://127.0.0.1:8910",
TokenDefaultTTL: time.Hour,
})
if _, err := store.RunScan(ctx, defaultScanSourceCode); err != nil {
t.Fatalf("run scan: %v", err)
}
created, err := store.CreateToken(ctx, CreateTokenInput{Name: "ttl", BasemapCodes: []string{"google_image"}})
if err != nil {
t.Fatalf("create token: %v", err)
}
if created.Meta.ExpiresAt == nil {
t.Fatal("expected a default expiry to be applied")
}
remaining := time.Until(*created.Meta.ExpiresAt)
if remaining < 55*time.Minute || remaining > 65*time.Minute {
t.Fatalf("expected ~1h expiry, got %s", remaining)
}
}
func TestPushRunsAsynchronously(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
tileRoot := filepath.Join(root, "tiles")
writeTestFile(t, filepath.Join(tileRoot, "google_image", "0", "0", "0.webp"), "tile")
received := make(chan map[string]any, 1)
callback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var payload map[string]any
_ = json.NewDecoder(r.Body).Decode(&payload)
select {
case received <- payload:
default:
}
w.WriteHeader(http.StatusOK)
}))
defer callback.Close()
store := openTestStore(t, StoreConfig{
SQLitePath: filepath.Join(root, "basemap.db"),
ScanRoot: tileRoot,
APIBaseURL: "http://127.0.0.1:8910",
TileBaseURL: "http://127.0.0.1:8910",
VectorBaseURL: "http://127.0.0.1:8910",
})
if _, err := store.RunScan(ctx, defaultScanSourceCode); err != nil {
t.Fatalf("run scan: %v", err)
}
if _, err := store.CreateTargetSystem(ctx, CreateTargetSystemInput{
Code: "downstream",
CallbackURL: callback.URL,
}); err != nil {
t.Fatalf("create target: %v", err)
}
record, err := store.PushBasemapVersion(ctx, "downstream", "google_image", "current")
if err != nil {
t.Fatalf("push: %v", err)
}
if record.Status != "pending" {
t.Fatalf("expected the push to be queued as pending, got %q", record.Status)
}
payload := waitForPayload(t, received)
if payload["event"] != "basemap.published" {
t.Fatalf("unexpected callback payload: %v", payload)
}
deadline := time.Now().Add(5 * time.Second)
for {
records, err := store.ListPushRecords(ctx, 5)
if err != nil {
t.Fatalf("list push records: %v", err)
}
done := false
for _, item := range records {
if item.ID == record.ID && item.Status == "succeeded" {
done = true
}
}
if done {
break
}
if time.Now().After(deadline) {
t.Fatalf("push record %s never reached succeeded: %+v", record.ID, records)
}
time.Sleep(25 * time.Millisecond)
}
}
func waitForPayload(t *testing.T, received <-chan map[string]any) map[string]any {
t.Helper()
select {
case payload := <-received:
return payload
case <-time.After(5 * time.Second):
t.Fatal("callback was never invoked")
return nil
}
}
func TestCloseWaitsForQueuedPush(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
tileRoot := filepath.Join(root, "tiles")
writeTestFile(t, filepath.Join(tileRoot, "google_image", "0", "0", "0.webp"), "tile")
started := make(chan struct{}, 1)
release := make(chan struct{})
callback := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
select {
case started <- struct{}{}:
default:
}
<-release
w.WriteHeader(http.StatusOK)
}))
defer callback.Close()
store, err := OpenStore(StoreConfig{
SQLitePath: filepath.Join(root, "basemap.db"),
ScanRoot: tileRoot,
APIBaseURL: "http://127.0.0.1:8910",
TileBaseURL: "http://127.0.0.1:8910",
VectorBaseURL: "http://127.0.0.1:8910",
})
if err != nil {
t.Fatalf("open store: %v", err)
}
if _, err := store.RunScan(ctx, defaultScanSourceCode); err != nil {
t.Fatalf("run scan: %v", err)
}
if _, err := store.CreateTargetSystem(ctx, CreateTargetSystemInput{Code: "downstream", CallbackURL: callback.URL}); err != nil {
t.Fatalf("create target: %v", err)
}
if _, err := store.PushBasemapVersion(ctx, "downstream", "google_image", "current"); err != nil {
t.Fatalf("push: %v", err)
}
<-started
closed := make(chan struct{})
go func() {
_ = store.Close()
close(closed)
}()
select {
case <-closed:
t.Fatal("Close returned while a push callback was still in flight")
case <-time.After(150 * time.Millisecond):
}
close(release)
select {
case <-closed:
case <-time.After(5 * time.Second):
t.Fatal("Close did not return after the push finished")
}
}
func TestScanRootDirectoryIsNotRequiredAtOpen(t *testing.T) {
root := t.TempDir()
store := openTestStore(t, StoreConfig{SQLitePath: filepath.Join(root, "basemap.db")})
if store.ScanRoot() != "" {
t.Fatalf("expected empty scan root, got %q", store.ScanRoot())
}
if _, err := os.Stat(filepath.Join(root, "basemap.db")); err != nil {
t.Fatalf("expected the database file to exist: %v", err)
}
}