- remove the events/outages module, ArcGIS adapter, portal config and event statuses - add a configurable city dictionary/parser and make City part of StreetKey - seed: read city/city_name, namespace IDs by city, keep structures distinct - fix reliability: schema v2 with fail-closed load, deterministic ordering, input limits, config validation, atomic DB publish, LLM response caps - add read-only shadow mode comparing the old ES path with addrkit - breaking: registry.db must be re-seeded; config has no portal section
218 lines
6.4 KiB
Go
218 lines
6.4 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"io"
|
||
"log/slog"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
|
||
"git.totmin.ru/en2zmax/addrkit/internal/domain/address"
|
||
"git.totmin.ru/en2zmax/addrkit/internal/domain/match"
|
||
)
|
||
|
||
func entry(t *testing.T, city, raw string) address.Entry {
|
||
t.Helper()
|
||
p := address.NewParser(address.NewDictionary(cityDictionary()), "").ParseInCity(raw, city)
|
||
return address.Entry{
|
||
ID: raw, Address: raw, District: "Тестовый",
|
||
City: p.City, CityKey: p.CityKey,
|
||
Ordinal: p.Ordinal, NameKey: p.NameKey, NameDisplay: p.NameDisplay,
|
||
StreetType: p.StreetType, StreetKey: p.StreetKey,
|
||
House: p.House, HouseFrac: p.HouseFrac, HouseLetter: p.HouseLetter,
|
||
Building: p.Building, BuildingFrac: p.BuildingFrac, BuildingLetter: p.BuildingLetter,
|
||
Structure: p.Structure,
|
||
}
|
||
}
|
||
|
||
func cityDictionary() []address.City {
|
||
return []address.City{
|
||
{Name: "Новосибирск", Aliases: []string{"нск", "новосиб", "новосибирская область"}},
|
||
{Name: "Бердск"},
|
||
}
|
||
}
|
||
|
||
func testResolver(t *testing.T) *match.Resolver {
|
||
t.Helper()
|
||
var entries []address.Entry
|
||
for _, a := range []string{"Станционная улица 1/1", "2-я Станционная улица 1/1", "Искитимская улица 5"} {
|
||
entries = append(entries, entry(t, "", a))
|
||
}
|
||
return match.NewResolver(match.NewIndex("test-version", entries, match.DefaultOptions()), nil)
|
||
}
|
||
|
||
func newServer(t *testing.T, resolver Resolver) *httptest.Server {
|
||
t.Helper()
|
||
srv := New(resolver, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||
ts := httptest.NewServer(srv.Handler())
|
||
t.Cleanup(ts.Close)
|
||
return ts
|
||
}
|
||
|
||
func postJSON(t *testing.T, url, body string) *http.Response {
|
||
t.Helper()
|
||
resp, err := http.Post(url, "application/json", bytes.NewBufferString(body))
|
||
if err != nil {
|
||
t.Fatalf("post: %v", err)
|
||
}
|
||
return resp
|
||
}
|
||
|
||
func TestResolveEndpoint(t *testing.T) {
|
||
ts := newServer(t, testResolver(t))
|
||
resp := postJSON(t, ts.URL+"/v1/resolve", `{"address":"2-я Станционная 1/1"}`)
|
||
defer resp.Body.Close()
|
||
|
||
if resp.StatusCode != http.StatusOK {
|
||
t.Fatalf("status = %d", resp.StatusCode)
|
||
}
|
||
var res match.Result
|
||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||
t.Fatalf("decode: %v", err)
|
||
}
|
||
if res.Status != address.StatusFound || res.Address != "2-я Станционная улица 1/1" {
|
||
t.Fatalf("unexpected result: %+v", res)
|
||
}
|
||
}
|
||
|
||
func TestResolveValidation(t *testing.T) {
|
||
ts := newServer(t, testResolver(t))
|
||
for _, body := range []string{`{}`, `{"address":""}`, `not json`} {
|
||
resp := postJSON(t, ts.URL+"/v1/resolve", body)
|
||
resp.Body.Close()
|
||
if resp.StatusCode != http.StatusBadRequest {
|
||
t.Errorf("body %q: status = %d, want 400", body, resp.StatusCode)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestResolveRejectsOversizedAddress(t *testing.T) {
|
||
ts := newServer(t, testResolver(t))
|
||
body, _ := json.Marshal(map[string]string{"address": strings.Repeat("а", maxAddressRunes+1)})
|
||
resp := postJSON(t, ts.URL+"/v1/resolve", string(body))
|
||
resp.Body.Close()
|
||
if resp.StatusCode != http.StatusBadRequest {
|
||
t.Fatalf("status = %d, want 400", resp.StatusCode)
|
||
}
|
||
}
|
||
|
||
func TestResolveCityHint(t *testing.T) {
|
||
entries := []address.Entry{
|
||
entry(t, "Новосибирск", "Ленина улица 5"),
|
||
entry(t, "Бердск", "Ленина улица 5"),
|
||
}
|
||
resolver := match.NewResolver(
|
||
match.NewIndex("test", entries, match.DefaultOptions()),
|
||
nil,
|
||
)
|
||
ts := newServer(t, resolver)
|
||
|
||
resp := postJSON(t, ts.URL+"/v1/resolve", `{"address":"Ленина 5","city":"Бердск"}`)
|
||
defer resp.Body.Close()
|
||
var res match.Result
|
||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||
t.Fatalf("decode: %v", err)
|
||
}
|
||
if res.Status != address.StatusFound || res.City != "Бердск" || res.Address != "Ленина улица 5" {
|
||
t.Fatalf("unexpected result: %+v", res)
|
||
}
|
||
}
|
||
|
||
func TestCitiesEndpoint(t *testing.T) {
|
||
entries := []address.Entry{
|
||
entry(t, "Новосибирск", "Ленина улица 5"),
|
||
entry(t, "Бердск", "Ленина улица 5"),
|
||
}
|
||
resolver := match.NewResolver(match.NewIndex("test", entries, match.DefaultOptions()), nil)
|
||
ts := newServer(t, resolver)
|
||
|
||
resp, err := http.Get(ts.URL + "/v1/cities")
|
||
if err != nil {
|
||
t.Fatalf("get: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
var body struct {
|
||
Cities []string `json:"cities"`
|
||
}
|
||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||
t.Fatalf("decode: %v", err)
|
||
}
|
||
if len(body.Cities) != 2 || body.Cities[0] != "Бердск" || body.Cities[1] != "Новосибирск" {
|
||
t.Fatalf("cities = %v", body.Cities)
|
||
}
|
||
}
|
||
|
||
func TestCityEndpoint(t *testing.T) {
|
||
parser := address.NewParser(address.NewDictionary(cityDictionary()), "")
|
||
resolver := match.NewResolverWithOptions(
|
||
match.NewIndex("test", nil, match.DefaultOptions()),
|
||
nil,
|
||
match.ResolverOptions{Parser: parser},
|
||
)
|
||
ts := newServer(t, resolver)
|
||
|
||
for _, tt := range []struct {
|
||
query string
|
||
city string
|
||
}{
|
||
{"нск", "Новосибирск"},
|
||
{"г. Новосибирск", "Новосибирск"},
|
||
{"Новосибирская область", "Новосибирск"},
|
||
{"Бердск", "Бердск"},
|
||
{"Томск", ""},
|
||
} {
|
||
body, _ := json.Marshal(map[string]string{"query": tt.query})
|
||
resp := postJSON(t, ts.URL+"/v1/city", string(body))
|
||
var got cityResponse
|
||
if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
|
||
resp.Body.Close()
|
||
t.Fatalf("%q: decode: %v", tt.query, err)
|
||
}
|
||
resp.Body.Close()
|
||
if tt.city == "" {
|
||
if got.Status != "not_found" {
|
||
t.Errorf("%q: status = %q, want not_found", tt.query, got.Status)
|
||
}
|
||
continue
|
||
}
|
||
if got.Status != "found" || got.City != tt.city {
|
||
t.Errorf("%q: %+v", tt.query, got)
|
||
}
|
||
}
|
||
|
||
resp := postJSON(t, ts.URL+"/v1/city", `{}`)
|
||
resp.Body.Close()
|
||
if resp.StatusCode != http.StatusBadRequest {
|
||
t.Errorf("empty query status = %d, want 400", resp.StatusCode)
|
||
}
|
||
}
|
||
|
||
func TestReadyzAndVersion(t *testing.T) {
|
||
ts := newServer(t, testResolver(t))
|
||
|
||
resp, err := http.Get(ts.URL + "/v1/readyz")
|
||
if err != nil {
|
||
t.Fatalf("readyz: %v", err)
|
||
}
|
||
resp.Body.Close()
|
||
if resp.StatusCode != http.StatusOK {
|
||
t.Fatalf("readyz status = %d", resp.StatusCode)
|
||
}
|
||
|
||
resp, err = http.Get(ts.URL + "/v1/version")
|
||
if err != nil {
|
||
t.Fatalf("version: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
var body map[string]string
|
||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||
t.Fatalf("decode: %v", err)
|
||
}
|
||
if body["data_version"] != "test-version" {
|
||
t.Fatalf("data_version = %q", body["data_version"])
|
||
}
|
||
}
|