Files
fire-safety-ymd/internal/repository/postgis_test.go
T
2026-09-05 15:46:37 +08:00

232 lines
6.7 KiB
Go

package repository
import (
"context"
"slices"
"strings"
"testing"
"time"
)
func TestOpenPostGISDoesNotExposeInvalidDSN(t *testing.T) {
secret := "do-not-leak"
_, err := OpenPostGIS(context.Background(), PostGISOptions{
DSN: "postgresql://user:" + secret + "%zz@localhost/database",
MaxConns: 1,
ConnectTimeout: time.Second,
QueryTimeout: time.Second,
})
if err == nil {
t.Fatal("OpenPostGIS() error = nil")
}
if strings.Contains(err.Error(), secret) {
t.Fatalf("OpenPostGIS() leaked DSN content: %v", err)
}
}
func TestValidateReadinessAcceptsExpectedGeometryContracts(t *testing.T) {
report := validReadinessReport()
if err := validateReadiness(report, 4326); err != nil {
t.Fatalf("validateReadiness() error = %v", err)
}
}
func TestValidateReadinessRejectsUnsafeSpatialMetadata(t *testing.T) {
tests := []struct {
name string
mutate func(*ReadinessReport)
want string
}{
{
name: "SRID zero",
mutate: func(report *ReadinessReport) {
report.Tables[0].SRIDs = []int{0}
},
want: "unexpected SRID 0",
},
{
name: "mixed SRID",
mutate: func(report *ReadinessReport) {
report.Tables[1].SRIDs = []int{4326, 3857}
},
want: "unexpected SRID 3857",
},
{
name: "unexpected geometry type",
mutate: func(report *ReadinessReport) {
report.Tables[2].GeometryTypes = []string{"POLYGON"}
},
want: "unexpected geometry type POLYGON",
},
{
name: "coordinate outside WGS84",
mutate: func(report *ReadinessReport) {
report.Tables[0].OutOfBoundsCount = 1
},
want: "outside WGS84 bounds",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
report := validReadinessReport()
tt.mutate(&report)
err := validateReadiness(report, 4326)
if err == nil || !strings.Contains(err.Error(), tt.want) {
t.Fatalf("validateReadiness() error = %v, want %q", err, tt.want)
}
})
}
}
func TestValidateReadinessAllowsInvalidGeometriesThatQueriesExclude(t *testing.T) {
report := validReadinessReport()
report.Tables[5].InvalidCount = 4
report.Tables[6].InvalidCount = 4
report.Tables[7].InvalidCount = 27
if err := validateReadiness(report, 4326); err != nil {
t.Fatalf("validateReadiness() error = %v", err)
}
}
func TestTableAuditWarningsExposeExcludedInvalidGeometries(t *testing.T) {
audit := TableAudit{
Table: "st_2_fanghuowangge",
RowCount: 2,
NullGeometryCount: 1,
EmptyGeometryCount: 1,
InvalidCount: 1,
HasGISTIndex: false,
}
warnings := tableAuditWarnings(audit)
for _, want := range []string{
"st_2_fanghuowangge:null_geometries_excluded",
"st_2_fanghuowangge:empty_geometries_excluded",
"st_2_fanghuowangge:invalid_geometries_excluded",
"st_2_fanghuowangge:missing_gist_geometry_index",
} {
if !slices.Contains(warnings, want) {
t.Fatalf("warnings = %#v, want %q", warnings, want)
}
}
}
func TestSpatialQueriesKeepScopeAndBoundsParameterized(t *testing.T) {
nearbyQueries := []struct {
name string
query string
wantScopePredicates int
}{
{name: "water sources", query: nearbyWaterSourcesSQL, wantScopePredicates: 2},
{name: "command posts", query: commandPostCandidatesSQL, wantScopePredicates: 2},
{name: "access lines", query: nearbyAccessLinesSQL, wantScopePredicates: 1},
{name: "risk areas", query: nearbyRiskAreasSQL, wantScopePredicates: 2},
}
for _, tt := range nearbyQueries {
query := tt.query
for _, required := range []string{"$3::boolean", "ANY($4::text[])", "$5", "LIMIT $6", "ST_SRID", "ST_IsValid", "ST_IsEmpty", "ST_Expand"} {
if !strings.Contains(query, required) {
t.Fatalf("%s query lacks %q", tt.name, required)
}
}
if got := strings.Count(query, "($3::boolean OR"); got != tt.wantScopePredicates {
t.Fatalf("%s query has %d scope predicates, want %d", tt.name, got, tt.wantScopePredicates)
}
if got := strings.Count(query, "ANY($4::text[])"); got != tt.wantScopePredicates {
t.Fatalf("%s query has %d town predicates, want %d", tt.name, got, tt.wantScopePredicates)
}
}
for index, query := range []string{resolveIncidentContextSQL, responsibleUnitsSQL} {
for _, required := range []string{"$3::boolean", "ANY($4::text[])", "ST_SRID", "ST_IsValid", "ST_Covers"} {
if !strings.Contains(query, required) {
t.Fatalf("cover query %d lacks %q", index, required)
}
}
if got := strings.Count(query, "($3::boolean OR"); got != 1 {
t.Fatalf("cover query %d has %d scope predicates, want 1", index, got)
}
}
}
func TestPlaceCandidateSearchKeepsTermScopeAndLimitParameterized(t *testing.T) {
for _, required := range []string{
"lower($1::text)",
"($2::boolean OR",
"ANY($3::text[])",
"LIMIT $4",
"strpos",
"recorded_point",
"representative_point",
"ST_Force2D",
"ST_IsValid",
} {
if !strings.Contains(placeCandidateSearchSQL, required) {
t.Fatalf("place candidate query lacks %q", required)
}
}
if got := strings.Count(placeCandidateSearchSQL, "($2::boolean OR"); got != len(spatialTableSpecs) {
t.Fatalf("place candidate query has %d scope predicates, want %d", got, len(spatialTableSpecs))
}
if got := strings.Count(placeCandidateSearchSQL, "ST_IsValid("); got != len(spatialTableSpecs) {
t.Fatalf("place candidate query has %d validity filters, want %d", got, len(spatialTableSpecs))
}
for _, spec := range spatialTableSpecs {
if !strings.Contains(placeCandidateSearchSQL, "public."+spec.name) {
t.Fatalf("place candidate query does not include %s", spec.name)
}
}
}
func TestSpatialQueriesDoNotSelectSensitiveContactColumns(t *testing.T) {
queries := strings.Join([]string{
placeCandidateSearchSQL,
resolveIncidentContextSQL,
nearbyWaterSourcesSQL,
commandPostCandidatesSQL,
nearbyAccessLinesSQL,
responsibleUnitsSQL,
nearbyRiskAreasSQL,
}, "\n")
for _, forbidden := range []string{
"bpld",
"bplddh",
"csj",
"csjdh",
"lxdh",
"zjwgfzr",
"zjfzrlxfs",
"fhzddc",
"fhzddclxfs",
"chzbry",
"shzbry",
"zbry",
".bz",
} {
if strings.Contains(strings.ToLower(queries), strings.ToLower(forbidden)) {
t.Fatalf("spatial SQL references sensitive contact column %s", forbidden)
}
}
}
func TestQuoteIdentifierEscapesEmbeddedQuotes(t *testing.T) {
if got, want := quoteIdentifier(`safe"name`), `"safe""name"`; got != want {
t.Fatalf("quoteIdentifier() = %q, want %q", got, want)
}
}
func validReadinessReport() ReadinessReport {
report := ReadinessReport{Tables: make([]TableAudit, 0, len(spatialTableSpecs))}
for _, spec := range spatialTableSpecs {
report.Tables = append(report.Tables, TableAudit{
Table: spec.name,
RowCount: 2,
GeometryTypes: []string{spec.geometryTypes[0]},
SRIDs: []int{4326},
HasGISTIndex: true,
})
}
return report
}