ea9c746852
The hand-written table understood "какая погода в X" for six values of X. Ask about Kazan or Tbilisi and the city was dropped silently and answered for the default location — a correct-sounding answer about the wrong place. The table is gone. internal/weather already calls Open-Meteo's geocoding endpoint on every lookup, so the place he named goes straight there and any place it knows is a place he can ask about. He speaks the prepositional case, so locationCandidates reverses the two endings that cover most of it: a final "е" is a nominative "а" or nothing, a final "и" is a soft sign. A wrong candidate finds no city; it never invents one. A place the geocoder does not have now reads as "не знаю такого города" rather than as a provider outage or, worse, as the default city's weather. ErrLocationUnknown is what carries that apart. "в" followed by a room or a day word is still the default location. Those questions are answered by the house sensors and the calendar, not by Open-Meteo, and they must not be read as a city.
112 lines
3.0 KiB
Go
112 lines
3.0 KiB
Go
package weather
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
)
|
|
|
|
func TestOpenMeteoProvider(t *testing.T) {
|
|
var geoReqCount, weatherReqCount int
|
|
mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/v1/search" {
|
|
geoReqCount++
|
|
json.NewEncoder(w).Encode(geoResponse{
|
|
Results: []geoResult{{
|
|
Name: "Moscow", Latitude: 55.7558, Longitude: 37.6173, Country: "Russia",
|
|
}},
|
|
})
|
|
return
|
|
}
|
|
if r.URL.Path == "/v1/forecast" {
|
|
weatherReqCount++
|
|
json.NewEncoder(w).Encode(forecastResponse{
|
|
CurrentWeather: currentWeather{
|
|
Temperature: 22.5,
|
|
WMO: 0,
|
|
WindSpeed: 3.2,
|
|
},
|
|
})
|
|
return
|
|
}
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}))
|
|
defer mock.Close()
|
|
|
|
p := NewOpenMeteoProvider()
|
|
transport := &mockTransport{
|
|
geoURL: mock.URL + "/v1/search",
|
|
forecastURL: mock.URL + "/v1/forecast",
|
|
}
|
|
p.SetClient(&http.Client{Transport: transport})
|
|
|
|
w, err := p.CurrentWeather(context.Background(), "Moscow")
|
|
if err != nil {
|
|
t.Fatalf("CurrentWeather: %v", err)
|
|
}
|
|
if w.Location != "Moscow" {
|
|
t.Errorf("location = %q, want Moscow", w.Location)
|
|
}
|
|
if w.Temperature != 22.5 {
|
|
t.Errorf("temperature = %f, want 22.5", w.Temperature)
|
|
}
|
|
if w.Condition != "ясно" {
|
|
t.Errorf("condition = %q, want ясно", w.Condition)
|
|
}
|
|
}
|
|
|
|
type mockTransport struct {
|
|
geoURL string
|
|
forecastURL string
|
|
}
|
|
|
|
func (t *mockTransport) RoundTrip(r *http.Request) (*http.Response, error) {
|
|
var targetURL string
|
|
if r.URL.Host == "geocoding-api.open-meteo.com" {
|
|
targetURL = t.geoURL + "?" + r.URL.RawQuery
|
|
} else if r.URL.Host == "api.open-meteo.com" {
|
|
targetURL = t.forecastURL + "?" + r.URL.RawQuery
|
|
} else {
|
|
return nil, fmt.Errorf("unexpected host: %s", r.URL.Host)
|
|
}
|
|
req, _ := http.NewRequestWithContext(r.Context(), r.Method, targetURL, nil)
|
|
return http.DefaultTransport.RoundTrip(req)
|
|
}
|
|
|
|
func TestStubProvider(t *testing.T) {
|
|
p := NewStubProvider()
|
|
_, err := p.CurrentWeather(context.Background(), "Moscow")
|
|
if err != ErrNotConfigured {
|
|
t.Fatalf("StubProvider: want ErrNotConfigured, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestLocationCandidates — he speaks the prepositional case and the geocoder
|
|
// wants the nominative (Vikunja #421).
|
|
func TestLocationCandidates(t *testing.T) {
|
|
cases := map[string][]string{
|
|
"Москве": {"Москве", "Москва", "Москв"},
|
|
"Казани": {"Казани", "Казань", "Казан"},
|
|
"Лондоне": {"Лондоне", "Лондона", "Лондон"},
|
|
"Тбилиси": {"Тбилиси", "Тбились", "Тбилис"},
|
|
"Berlin": {"Berlin"},
|
|
"Уфе": {"Уфе"}, // too short to strip — asked as spoken
|
|
}
|
|
for in, want := range cases {
|
|
got := locationCandidates(in)
|
|
if len(got) != len(want) {
|
|
t.Errorf("locationCandidates(%q) = %v, want %v", in, got, want)
|
|
continue
|
|
}
|
|
for i := range got {
|
|
if got[i] != want[i] {
|
|
t.Errorf("locationCandidates(%q) = %v, want %v", in, got, want)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|