初始化第一版
This commit is contained in:
commit
8a6c31c14d
83 files changed
+14302
No files matched your search
@@ -0,0 +1,334 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"fire-safety-ymd/internal/domain"
|
||||
)
|
||||
|
||||
func TestSpatialServiceSearchesUserConfirmablePlaceCandidates(t *testing.T) {
|
||||
var captured domain.PlaceSearchQuery
|
||||
repository := &fakeSpatialRepository{
|
||||
places: func(_ context.Context, query domain.PlaceSearchQuery) ([]domain.PlaceCandidate, error) {
|
||||
captured = query
|
||||
return []domain.PlaceCandidate{{
|
||||
SourceRecordID: "station-1",
|
||||
PlaceType: "fire_check_station",
|
||||
Name: "观水检查站",
|
||||
MatchedField: "name",
|
||||
MatchedText: "观水检查站",
|
||||
MatchKind: "partial",
|
||||
LocationKind: "recorded_point",
|
||||
}}, nil
|
||||
},
|
||||
}
|
||||
spatial, err := NewSpatialService(repository, domain.SpatialScope{AllTowns: true}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
|
||||
result, err := spatial.SearchPlaceCandidates(context.Background(), " 观水 ", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("SearchPlaceCandidates() error = %v", err)
|
||||
}
|
||||
if captured.PlaceName != "观水" || captured.Limit != 10 || !captured.Scope.AllTowns {
|
||||
t.Fatalf("captured query = %#v", captured)
|
||||
}
|
||||
if result.Status != "ok" || result.Metadata.ResultCount != 1 || len(result.Data) != 1 {
|
||||
t.Fatalf("result = %#v", result)
|
||||
}
|
||||
for _, warning := range []string{
|
||||
invalidGeometryWarning,
|
||||
"place_search_is_limited_to_existing_fire_safety_records",
|
||||
"place_candidates_require_user_confirmation",
|
||||
"representative_points_are_not_exact_incident_locations",
|
||||
} {
|
||||
if !containsString(result.Warnings, warning) {
|
||||
t.Fatalf("warnings = %#v, want %q", result.Warnings, warning)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpatialServiceRejectsInvalidPlaceSearch(t *testing.T) {
|
||||
spatial, err := NewSpatialService(&fakeSpatialRepository{}, domain.SpatialScope{AllTowns: true}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
placeName string
|
||||
limit int
|
||||
}{
|
||||
{name: "empty", placeName: " "},
|
||||
{name: "too short", placeName: "山"},
|
||||
{name: "too long", placeName: strings.Repeat("山", 101)},
|
||||
{name: "control character", placeName: "观水\n镇"},
|
||||
{name: "limit", placeName: "观水镇", limit: 21},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := spatial.SearchPlaceCandidates(context.Background(), tt.placeName, tt.limit)
|
||||
if !errors.Is(err, ErrInvalidArgument) {
|
||||
t.Fatalf("error = %v, want ErrInvalidArgument", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpatialServiceUsesTrustedScopeAndDefaults(t *testing.T) {
|
||||
var captured domain.NearbyQuery
|
||||
repository := &fakeSpatialRepository{
|
||||
water: func(_ context.Context, query domain.NearbyQuery) ([]domain.WaterSource, error) {
|
||||
captured = query
|
||||
return []domain.WaterSource{{SourceRecordID: "152", Category: "water_source"}}, nil
|
||||
},
|
||||
}
|
||||
towns := []string{"莒格庄镇"}
|
||||
spatial, err := NewSpatialService(repository, domain.SpatialScope{AllowedTowns: towns}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
towns[0] = "被调用方篡改"
|
||||
fixedNow := time.Date(2026, 9, 4, 8, 30, 0, 0, time.FixedZone("CST", 8*60*60))
|
||||
spatial.now = func() time.Time { return fixedNow }
|
||||
|
||||
result, err := spatial.FindNearbyWaterSources(context.Background(), domain.Coordinate{Longitude: 121.75, Latitude: 37.19}, 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("FindNearbyWaterSources() error = %v", err)
|
||||
}
|
||||
if captured.RadiusMeters != 10_000 || captured.Limit != 10 {
|
||||
t.Fatalf("captured bounds = radius %.0f limit %d", captured.RadiusMeters, captured.Limit)
|
||||
}
|
||||
if captured.Scope.AllTowns || len(captured.Scope.AllowedTowns) != 1 || captured.Scope.AllowedTowns[0] != "莒格庄镇" {
|
||||
t.Fatalf("trusted scope = %#v", captured.Scope)
|
||||
}
|
||||
if result.Status != "ok" || result.Metadata.ResultCount != 1 || result.Metadata.GeneratedAt != "2026-09-04T00:30:00Z" {
|
||||
t.Fatalf("unexpected result metadata: %#v", result)
|
||||
}
|
||||
if result.Metadata.SearchRadiusMeters == nil || *result.Metadata.SearchRadiusMeters != 10_000 {
|
||||
t.Fatalf("search radius metadata = %#v", result.Metadata.SearchRadiusMeters)
|
||||
}
|
||||
if got, want := result.Metadata.DataSources, []string{"water_source", "storage_pool"}; len(got) != len(want) || got[0] != want[0] || got[1] != want[1] {
|
||||
t.Fatalf("data sources = %#v, want %#v", got, want)
|
||||
}
|
||||
if !containsString(result.Warnings, townAllowlistScopeWarning) {
|
||||
t.Fatalf("warnings = %#v, want town allowlist scope warning", result.Warnings)
|
||||
}
|
||||
if !containsString(result.Warnings, invalidGeometryWarning) {
|
||||
t.Fatalf("warnings = %#v, want invalid-geometry exclusion warning", result.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpatialServiceUsesAllTownsScope(t *testing.T) {
|
||||
var captured domain.NearbyQuery
|
||||
repository := &fakeSpatialRepository{
|
||||
water: func(_ context.Context, query domain.NearbyQuery) ([]domain.WaterSource, error) {
|
||||
captured = query
|
||||
return []domain.WaterSource{}, nil
|
||||
},
|
||||
}
|
||||
spatial, err := NewSpatialService(repository, domain.SpatialScope{AllTowns: true}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
|
||||
result, err := spatial.FindNearbyWaterSources(context.Background(), domain.Coordinate{Longitude: 121.75, Latitude: 37.19}, 100, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("FindNearbyWaterSources() error = %v", err)
|
||||
}
|
||||
if !captured.Scope.AllTowns || len(captured.Scope.AllowedTowns) != 0 {
|
||||
t.Fatalf("trusted scope = %#v, want all towns", captured.Scope)
|
||||
}
|
||||
if !containsString(result.Warnings, allTownsScopeWarning) || containsString(result.Warnings, townAllowlistScopeWarning) {
|
||||
t.Fatalf("warnings = %#v, want only all-towns scope warning", result.Warnings)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewSpatialServiceRejectsInvalidScope(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
scope domain.SpatialScope
|
||||
}{
|
||||
{name: "empty allowlist", scope: domain.SpatialScope{}},
|
||||
{name: "all with allowlist", scope: domain.SpatialScope{AllTowns: true, AllowedTowns: []string{"莒格庄镇"}}},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if _, err := NewSpatialService(&fakeSpatialRepository{}, tt.scope, time.Second); err == nil {
|
||||
t.Fatal("NewSpatialService() error = nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpatialServiceReturnsExplicitNoResults(t *testing.T) {
|
||||
spatial, err := NewSpatialService(&fakeSpatialRepository{}, domain.SpatialScope{AllowedTowns: []string{"高陵镇"}}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
|
||||
result, err := spatial.ResolveIncidentContext(context.Background(), domain.Coordinate{Longitude: 121, Latitude: 37})
|
||||
if err != nil {
|
||||
t.Fatalf("ResolveIncidentContext() error = %v", err)
|
||||
}
|
||||
if result.Status != "no_results" || result.Data == nil || len(result.Data) != 0 || result.Metadata.ResultCount != 0 {
|
||||
t.Fatalf("unexpected no-results response: %#v", result)
|
||||
}
|
||||
if len(result.Warnings) == 0 {
|
||||
t.Fatal("no-results response must retain data-quality warnings")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpatialServiceRejectsUnboundedInputs(t *testing.T) {
|
||||
repository := &fakeSpatialRepository{}
|
||||
spatial, err := NewSpatialService(repository, domain.SpatialScope{AllowedTowns: []string{"高陵镇"}}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
call func() error
|
||||
}{
|
||||
{
|
||||
name: "longitude",
|
||||
call: func() error {
|
||||
_, err := spatial.ResolveIncidentContext(context.Background(), domain.Coordinate{Longitude: 181, Latitude: 37})
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "water radius",
|
||||
call: func() error {
|
||||
_, err := spatial.FindNearbyWaterSources(context.Background(), domain.Coordinate{Longitude: 121, Latitude: 37}, 30_001, 1)
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "access radius",
|
||||
call: func() error {
|
||||
_, err := spatial.ListNearbyAccessLines(context.Background(), domain.Coordinate{Longitude: 121, Latitude: 37}, 10_001, 1)
|
||||
return err
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "limit",
|
||||
call: func() error {
|
||||
_, err := spatial.FindNearbyRiskAreas(context.Background(), domain.Coordinate{Longitude: 121, Latitude: 37}, 100, 21)
|
||||
return err
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.call(); !errors.Is(err, ErrInvalidArgument) {
|
||||
t.Fatalf("error = %v, want ErrInvalidArgument", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSpatialServiceClassifiesRepositoryFailures(t *testing.T) {
|
||||
t.Run("data source", func(t *testing.T) {
|
||||
spatial, err := NewSpatialService(&fakeSpatialRepository{
|
||||
water: func(context.Context, domain.NearbyQuery) ([]domain.WaterSource, error) {
|
||||
return nil, errors.New("driver detail must stay internal")
|
||||
},
|
||||
}, domain.SpatialScope{AllowedTowns: []string{"高陵镇"}}, time.Second)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
_, err = spatial.FindNearbyWaterSources(context.Background(), domain.Coordinate{Longitude: 121, Latitude: 37}, 100, 1)
|
||||
if !errors.Is(err, ErrDataSourceUnavailable) {
|
||||
t.Fatalf("error = %v, want ErrDataSourceUnavailable", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("timeout", func(t *testing.T) {
|
||||
spatial, err := NewSpatialService(&fakeSpatialRepository{
|
||||
water: func(ctx context.Context, _ domain.NearbyQuery) ([]domain.WaterSource, error) {
|
||||
<-ctx.Done()
|
||||
return nil, ctx.Err()
|
||||
},
|
||||
}, domain.SpatialScope{AllowedTowns: []string{"高陵镇"}}, 5*time.Millisecond)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSpatialService() error = %v", err)
|
||||
}
|
||||
_, err = spatial.FindNearbyWaterSources(context.Background(), domain.Coordinate{Longitude: 121, Latitude: 37}, 100, 1)
|
||||
if !errors.Is(err, ErrQueryTimeout) {
|
||||
t.Fatalf("error = %v, want ErrQueryTimeout", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func containsString(values []string, target string) bool {
|
||||
for _, value := range values {
|
||||
if value == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type fakeSpatialRepository struct {
|
||||
places func(context.Context, domain.PlaceSearchQuery) ([]domain.PlaceCandidate, error)
|
||||
resolve func(context.Context, domain.Coordinate, domain.SpatialScope) ([]domain.IncidentContext, error)
|
||||
water func(context.Context, domain.NearbyQuery) ([]domain.WaterSource, error)
|
||||
commandPost func(context.Context, domain.NearbyQuery) ([]domain.CommandPostCandidate, error)
|
||||
access func(context.Context, domain.NearbyQuery) ([]domain.AccessLine, error)
|
||||
units func(context.Context, domain.Coordinate, domain.SpatialScope) ([]domain.ResponsibleUnit, error)
|
||||
risks func(context.Context, domain.NearbyQuery) ([]domain.RiskArea, error)
|
||||
}
|
||||
|
||||
func (f *fakeSpatialRepository) SearchPlaceCandidates(ctx context.Context, query domain.PlaceSearchQuery) ([]domain.PlaceCandidate, error) {
|
||||
if f.places == nil {
|
||||
return []domain.PlaceCandidate{}, nil
|
||||
}
|
||||
return f.places(ctx, query)
|
||||
}
|
||||
|
||||
func (f *fakeSpatialRepository) ResolveIncidentContext(ctx context.Context, point domain.Coordinate, scope domain.SpatialScope) ([]domain.IncidentContext, error) {
|
||||
if f.resolve == nil {
|
||||
return []domain.IncidentContext{}, nil
|
||||
}
|
||||
return f.resolve(ctx, point, scope)
|
||||
}
|
||||
|
||||
func (f *fakeSpatialRepository) FindNearbyWaterSources(ctx context.Context, query domain.NearbyQuery) ([]domain.WaterSource, error) {
|
||||
if f.water == nil {
|
||||
return []domain.WaterSource{}, nil
|
||||
}
|
||||
return f.water(ctx, query)
|
||||
}
|
||||
|
||||
func (f *fakeSpatialRepository) FindCommandPostCandidates(ctx context.Context, query domain.NearbyQuery) ([]domain.CommandPostCandidate, error) {
|
||||
if f.commandPost == nil {
|
||||
return []domain.CommandPostCandidate{}, nil
|
||||
}
|
||||
return f.commandPost(ctx, query)
|
||||
}
|
||||
|
||||
func (f *fakeSpatialRepository) ListNearbyAccessLines(ctx context.Context, query domain.NearbyQuery) ([]domain.AccessLine, error) {
|
||||
if f.access == nil {
|
||||
return []domain.AccessLine{}, nil
|
||||
}
|
||||
return f.access(ctx, query)
|
||||
}
|
||||
|
||||
func (f *fakeSpatialRepository) GetResponsibleUnits(ctx context.Context, point domain.Coordinate, scope domain.SpatialScope) ([]domain.ResponsibleUnit, error) {
|
||||
if f.units == nil {
|
||||
return []domain.ResponsibleUnit{}, nil
|
||||
}
|
||||
return f.units(ctx, point, scope)
|
||||
}
|
||||
|
||||
func (f *fakeSpatialRepository) FindNearbyRiskAreas(ctx context.Context, query domain.NearbyQuery) ([]domain.RiskArea, error) {
|
||||
if f.risks == nil {
|
||||
return []domain.RiskArea{}, nil
|
||||
}
|
||||
return f.risks(ctx, query)
|
||||
}
|
||||
Reference in new issue
Block a user