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"]) } }