feat: Districts table and geojson stub
This commit is contained in:
@@ -20,6 +20,7 @@ import (
|
||||
type stubDatasetRepo struct {
|
||||
store map[uuid.UUID]domain.Dataset
|
||||
observations map[uuid.UUID][]domain.Observation
|
||||
districts map[string]domain.District
|
||||
createErr error
|
||||
deleted []uuid.UUID
|
||||
lastLimit, lastOffset int
|
||||
@@ -29,6 +30,7 @@ func newStubDatasetRepo() *stubDatasetRepo {
|
||||
return &stubDatasetRepo{
|
||||
store: map[uuid.UUID]domain.Dataset{},
|
||||
observations: map[uuid.UUID][]domain.Observation{},
|
||||
districts: map[string]domain.District{},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -155,6 +157,20 @@ func (r *stubDatasetRepo) CountObservations(_ context.Context, id uuid.UUID, _ *
|
||||
return len(r.observations[id]), nil
|
||||
}
|
||||
|
||||
func (r *stubDatasetRepo) ListAllObservations(_ context.Context, id uuid.UUID) ([]domain.Observation, error) {
|
||||
return r.observations[id], nil
|
||||
}
|
||||
|
||||
func (r *stubDatasetRepo) DistrictGeometriesByKato(_ context.Context, katos []string) (map[string]domain.District, error) {
|
||||
out := make(map[string]domain.District)
|
||||
for _, k := range katos {
|
||||
if d, ok := r.districts[k]; ok {
|
||||
out[k] = d
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// stubEnqueuer records parse, properties, extract, and convert enqueues.
|
||||
type stubEnqueuer struct {
|
||||
enqueued []uuid.UUID
|
||||
@@ -787,6 +803,267 @@ func TestDatasetService_Extract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_DistrictJoin(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
repo.store[id] = domain.Dataset{ID: id, FileType: domain.FileTypeVectorWithKato, Status: domain.DatasetStatusReady}
|
||||
|
||||
v2020, v2021 := 100.0, 150.0
|
||||
repo.observations[id] = []domain.Observation{
|
||||
{KatoCode: "710000000", Date: "2020-01-01", Value: &v2020},
|
||||
{KatoCode: "710000000", Date: "2021-01-01", Value: &v2021},
|
||||
{KatoCode: "999999999", Date: "2020-01-01", Value: &v2020}, // no district -> skipped
|
||||
}
|
||||
repo.districts["710000000"] = domain.District{
|
||||
Kato: "710000000", Name: "Astana",
|
||||
Geometry: json.RawMessage(`{"type":"Polygon","coordinates":[[[71,51],[72,51],[72,52],[71,51]]]}`),
|
||||
}
|
||||
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
fc, err := svc.GeoJSON(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if fc.Type != domain.GeoJSONFeatureCollection {
|
||||
t.Fatalf("type = %q", fc.Type)
|
||||
}
|
||||
if len(fc.Features) != 1 {
|
||||
t.Fatalf("want 1 feature (the unmatched KATO is skipped), got %d", len(fc.Features))
|
||||
}
|
||||
f := fc.Features[0]
|
||||
if f.Type != domain.GeoJSONFeature {
|
||||
t.Fatalf("feature type = %q", f.Type)
|
||||
}
|
||||
if len(f.Geometry) == 0 {
|
||||
t.Fatal("feature geometry should come from the district")
|
||||
}
|
||||
if f.Properties["kato"] != "710000000" || f.Properties["name"] != "Astana" {
|
||||
t.Fatalf("unexpected properties: %+v", f.Properties)
|
||||
}
|
||||
if f.Properties["2020-01-01"] != 100.0 || f.Properties["2021-01-01"] != 150.0 {
|
||||
t.Fatalf("year values not flattened into properties: %+v", f.Properties)
|
||||
}
|
||||
|
||||
// The whole thing must marshal to valid GeoJSON.
|
||||
if _, err := json.Marshal(fc); err != nil {
|
||||
t.Fatalf("feature collection not valid JSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_UsesDatasetGeometry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
geom := json.RawMessage(`{"type":"MultiPolygon","coordinates":[]}`)
|
||||
repo.store[id] = domain.Dataset{
|
||||
ID: id, FileType: domain.FileTypeVectorWithKato, Status: domain.DatasetStatusReady,
|
||||
Name: "Population", Geometry: geom,
|
||||
}
|
||||
v := 42.0
|
||||
repo.observations[id] = []domain.Observation{
|
||||
{KatoCode: "710000000", Date: "2020-01-01", Value: &v},
|
||||
{KatoCode: "710000000", Date: "2021-01-01", Value: &v},
|
||||
}
|
||||
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
fc, err := svc.GeoJSON(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(fc.Features) != 1 {
|
||||
t.Fatalf("want a single feature wrapping the dataset geometry, got %d", len(fc.Features))
|
||||
}
|
||||
f := fc.Features[0]
|
||||
if string(f.Geometry) != string(geom) {
|
||||
t.Fatalf("feature should reuse dataset geometry, got %s", f.Geometry)
|
||||
}
|
||||
// Properties hold only the observations, keyed by KATO at the top level.
|
||||
if _, ok := f.Properties["name"]; ok {
|
||||
t.Fatalf("properties should contain only observations, got metadata: %+v", f.Properties)
|
||||
}
|
||||
kato, ok := f.Properties["710000000"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("observations not keyed by KATO at top level: %+v", f.Properties)
|
||||
}
|
||||
if kato["2020-01-01"] != 42.0 || kato["2021-01-01"] != 42.0 {
|
||||
t.Fatalf("year values missing under KATO: %+v", kato)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_KatoGeoJSON_IgnoresDatasetGeometry(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
// Dataset HAS its own geometry, which KatoGeoJSON must ignore entirely.
|
||||
repo.store[id] = domain.Dataset{
|
||||
ID: id, FileType: domain.FileTypeVectorWithKato, Status: domain.DatasetStatusReady,
|
||||
Name: "Population", Geometry: json.RawMessage(`{"type":"MultiPolygon","coordinates":[[[[0,0]]]]}`),
|
||||
}
|
||||
v := 100.0
|
||||
repo.observations[id] = []domain.Observation{
|
||||
{KatoCode: "710000000", Date: "2020-01-01", Value: &v},
|
||||
{KatoCode: "999999999", Date: "2020-01-01", Value: &v}, // no district -> skipped
|
||||
}
|
||||
district := json.RawMessage(`{"type":"Polygon","coordinates":[[[71,51],[72,51],[72,52],[71,51]]]}`)
|
||||
repo.districts["710000000"] = domain.District{Kato: "710000000", Name: "Astana", Geometry: district}
|
||||
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
fc, err := svc.KatoGeoJSON(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(fc.Features) != 1 {
|
||||
t.Fatalf("want 1 feature (unmatched KATO skipped), got %d", len(fc.Features))
|
||||
}
|
||||
f := fc.Features[0]
|
||||
if string(f.Geometry) != string(district) {
|
||||
t.Fatalf("geometry must come from the district, not the dataset: %s", f.Geometry)
|
||||
}
|
||||
if f.Properties["kato"] != "710000000" || f.Properties["name"] != "Astana" {
|
||||
t.Fatalf("unexpected properties: %+v", f.Properties)
|
||||
}
|
||||
if f.Properties["2020-01-01"] != 100.0 {
|
||||
t.Fatalf("observation value not mapped onto polygon: %+v", f.Properties)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_KatoGeoJSON_ConflictWhenNotReady(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
repo.store[id] = domain.Dataset{ID: id, FileType: domain.FileTypeVectorWithKato, Status: domain.DatasetStatusParsing}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
if _, err := svc.KatoGeoJSON(context.Background(), id); !errors.Is(err, domain.ErrConflict) {
|
||||
t.Fatalf("want ErrConflict for non-ready dataset, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_ConflictWhenNotReady(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
repo.store[id] = domain.Dataset{ID: id, FileType: domain.FileTypeVectorWithKato, Status: domain.DatasetStatusExtracting}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
if _, err := svc.GeoJSON(context.Background(), id); !errors.Is(err, domain.ErrConflict) {
|
||||
t.Fatalf("want ErrConflict for non-ready dataset, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_RejectsRaster(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
repo.store[id] = domain.Dataset{ID: id, FileType: domain.FileTypeRaster, Status: domain.DatasetStatusReady}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
if _, err := svc.GeoJSON(context.Background(), id); !errors.Is(err, domain.ErrValidation) {
|
||||
t.Fatalf("want ErrValidation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_Vector_GeometryOnly(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
geom := json.RawMessage(`{"type":"MultiPolygon","coordinates":[[[[1,1],[2,2],[3,1],[1,1]]]]}`)
|
||||
repo.store[id] = domain.Dataset{
|
||||
ID: id, FileType: domain.FileTypeVector, Status: domain.DatasetStatusReady,
|
||||
Name: "Roads", Geometry: geom,
|
||||
}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
fc, err := svc.GeoJSON(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(fc.Features) != 1 {
|
||||
t.Fatalf("want a single geometry-only feature, got %d", len(fc.Features))
|
||||
}
|
||||
f := fc.Features[0]
|
||||
if string(f.Geometry) != string(geom) {
|
||||
t.Fatalf("feature should reuse the dataset geometry, got %s", f.Geometry)
|
||||
}
|
||||
if len(f.Properties) != 0 {
|
||||
t.Fatalf("vector feature should have empty properties, got %+v", f.Properties)
|
||||
}
|
||||
if _, err := json.Marshal(fc); err != nil {
|
||||
t.Fatalf("feature collection not valid JSON: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_Vector_TableDataAsProperties(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
geom := json.RawMessage(`{"type":"Polygon","coordinates":[[[1,1],[2,2],[3,1],[1,1]]]}`)
|
||||
repo.store[id] = domain.Dataset{
|
||||
ID: id, FileType: domain.FileTypeVector, Status: domain.DatasetStatusReady,
|
||||
Geometry: geom,
|
||||
Properties: json.RawMessage(`[{"name":"Astana","pop":"1000"}]`), // single gpkg row
|
||||
}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
fc, err := svc.GeoJSON(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(fc.Features) != 1 {
|
||||
t.Fatalf("want 1 feature, got %d", len(fc.Features))
|
||||
}
|
||||
props := fc.Features[0].Properties
|
||||
if props["name"] != "Astana" || props["pop"] != "1000" {
|
||||
t.Fatalf("table data not exposed as top-level properties: %+v", props)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_Vector_MultiRowTableData(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
geom := json.RawMessage(`{"type":"Polygon","coordinates":[[[1,1],[2,2],[3,1],[1,1]]]}`)
|
||||
repo.store[id] = domain.Dataset{
|
||||
ID: id, FileType: domain.FileTypeVector, Status: domain.DatasetStatusReady,
|
||||
Geometry: geom,
|
||||
Properties: json.RawMessage(`[{"name":"Astana"},{"name":"Almaty"}]`),
|
||||
}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
fc, err := svc.GeoJSON(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
rows, ok := fc.Features[0].Properties["rows"].([]map[string]any)
|
||||
if !ok || len(rows) != 2 {
|
||||
t.Fatalf("multi-row table data not kept under \"rows\": %+v", fc.Features[0].Properties)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_GeoJSON_Vector_NoGeometry(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
repo.store[id] = domain.Dataset{ID: id, FileType: domain.FileTypeVector, Status: domain.DatasetStatusReady}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
fc, err := svc.GeoJSON(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if fc.Type != domain.GeoJSONFeatureCollection {
|
||||
t.Fatalf("type = %q", fc.Type)
|
||||
}
|
||||
if len(fc.Features) != 0 {
|
||||
t.Fatalf("want empty feature collection when vector has no geometry, got %d", len(fc.Features))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_KatoGeoJSON_RejectsVector(t *testing.T) {
|
||||
id := uuid.New()
|
||||
repo := newStubDatasetRepo()
|
||||
repo.store[id] = domain.Dataset{ID: id, FileType: domain.FileTypeVector, Status: domain.DatasetStatusReady}
|
||||
svc := newDatasetService(repo, &stubStore{}, true)
|
||||
|
||||
if _, err := svc.KatoGeoJSON(context.Background(), id); !errors.Is(err, domain.ErrValidation) {
|
||||
t.Fatalf("want ErrValidation for vector on .kato.geojson, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetService_ListSummaries_ClampsPaging(t *testing.T) {
|
||||
repo := newStubDatasetRepo()
|
||||
repo.store[uuid.New()] = domain.Dataset{}
|
||||
|
||||
Reference in New Issue
Block a user