aboutsummaryrefslogtreecommitdiff
path: root/cmd/prognosis/resolve_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/prognosis/resolve_test.go')
-rw-r--r--cmd/prognosis/resolve_test.go171
1 files changed, 171 insertions, 0 deletions
diff --git a/cmd/prognosis/resolve_test.go b/cmd/prognosis/resolve_test.go
new file mode 100644
index 0000000..f7f1347
--- /dev/null
+++ b/cmd/prognosis/resolve_test.go
@@ -0,0 +1,171 @@
+package main
+
+import (
+ "errors"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/lukaszkasprzak/prognosis/internal/cache"
+ "github.com/lukaszkasprzak/prognosis/internal/openmeteo"
+)
+
+func tmpStore(t *testing.T) *cache.Cache {
+ t.Helper()
+ return cache.New(filepath.Join(t.TempDir(), "cache.json"))
+}
+
+// stubGeocoder replaces the network for the duration of one test.
+func stubGeocoder(t *testing.T, cands []openmeteo.Candidate) *int {
+ t.Helper()
+ calls := 0
+ prev := geocode
+ geocode = func(string) ([]openmeteo.Candidate, error) {
+ calls++
+ return cands, nil
+ }
+ t.Cleanup(func() { geocode = prev })
+ return &calls
+}
+
+func twoWirys() []openmeteo.Candidate {
+ return []openmeteo.Candidate{
+ {Geo: cache.Geo{Lat: 52.3205, Lon: 16.8532, Label: "Wiry, PL", Country: "PL"}, Admin1: "Greater Poland"},
+ {Geo: cache.Geo{Lat: 50.8367, Lon: 16.6467, Label: "Wiry, PL", Country: "PL"}, Admin1: "Lower Silesia"},
+ }
+}
+
+func TestResolveRefusesAnAmbiguousName(t *testing.T) {
+ stubGeocoder(t, twoWirys())
+ _, err := resolve(tmpStore(t), "Wiry, PL", 0)
+ var amb *ambiguousError
+ if !errors.As(err, &amb) {
+ t.Fatalf("got err %v, want an ambiguousError: guessing is what sent the user to the wrong country", err)
+ }
+ if len(amb.candidates) != 2 {
+ t.Errorf("error carries %d candidates, want 2 so the user can choose", len(amb.candidates))
+ }
+}
+
+func TestResolveDoesNotCacheAnAmbiguousName(t *testing.T) {
+ stubGeocoder(t, twoWirys())
+ store := tmpStore(t)
+ if _, err := resolve(store, "Wiry, PL", 0); err == nil {
+ t.Fatal("expected a refusal")
+ }
+ if g, ok := store.Geo("Wiry, PL"); ok {
+ t.Errorf("cached %+v for an ambiguous name; a wrong guess would stick forever", g)
+ }
+}
+
+func TestResolvePickSelectsTheNthCandidate(t *testing.T) {
+ stubGeocoder(t, twoWirys())
+ got, err := resolve(tmpStore(t), "Wiry, PL", 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.Lat != 50.8367 {
+ t.Errorf("got lat %v, want 50.8367 (Lower Silesia, the second candidate)", got.Lat)
+ }
+}
+
+func TestResolvePickRemembersTheChoice(t *testing.T) {
+ stubGeocoder(t, twoWirys())
+ store := tmpStore(t)
+ if _, err := resolve(store, "Wiry, PL", 2); err != nil {
+ t.Fatal(err)
+ }
+ g, ok := store.Geo("Wiry, PL")
+ if !ok {
+ t.Fatal("a picked place was not cached, so the choice must be repeated every run")
+ }
+ if g.Lat != 50.8367 {
+ t.Errorf("cached lat %v, want the picked candidate's 50.8367", g.Lat)
+ }
+}
+
+func TestResolvePickOutOfRangeIsAnError(t *testing.T) {
+ stubGeocoder(t, twoWirys())
+ _, err := resolve(tmpStore(t), "Wiry, PL", 3)
+ if err == nil {
+ t.Fatal("expected an error: silently clamping would pick a place the user did not ask for")
+ }
+ // Every other bad flag value in this program exits 2; this must too, which
+ // means run() has to be able to tell it apart from a network failure.
+ var pe *pickError
+ if !errors.As(err, &pe) {
+ t.Errorf("got %T, want *pickError so run() can exit 2 rather than 1", err)
+ }
+}
+
+// A name resolved wrongly before this change is still in the cache, and the
+// cache is consulted first. Without this, --pick could never repair it.
+func TestResolvePickBypassesAPoisonedCacheEntry(t *testing.T) {
+ calls := stubGeocoder(t, twoWirys())
+ store := tmpStore(t)
+ poison := cache.Geo{Lat: 51.2417, Lon: 26.9411, Label: "Vyry, UA", Country: "UA"}
+ if err := store.PutGeo("Wiry, PL", poison); err != nil {
+ t.Fatal(err)
+ }
+ got, err := resolve(store, "Wiry, PL", 2)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if *calls == 0 {
+ t.Error("--pick used the cache instead of re-resolving, so a bad entry can never be corrected")
+ }
+ if got.Country != "PL" {
+ t.Errorf("got %+v, want the picked Polish candidate", got)
+ }
+ if g, _ := store.Geo("Wiry, PL"); g.Country != "PL" {
+ t.Errorf("cache still holds %+v; the pick should overwrite it", g)
+ }
+}
+
+func TestResolveCachesAnUnambiguousName(t *testing.T) {
+ only := []openmeteo.Candidate{
+ {Geo: cache.Geo{Lat: 50.0617, Lon: 19.9373, Label: "Krakow, PL", Country: "PL"}, Admin1: "Subcarpathia"},
+ }
+ calls := stubGeocoder(t, only)
+ store := tmpStore(t)
+ for i := 0; i < 2; i++ {
+ got, err := resolve(store, "Krakow", 0)
+ if err != nil {
+ t.Fatalf("run %d: %v", i+1, err)
+ }
+ if got.Label != "Krakow, PL" {
+ t.Fatalf("run %d: got %+v", i+1, got)
+ }
+ }
+ if *calls != 1 {
+ t.Errorf("geocoded %d times, want 1: the second run should hit the cache", *calls)
+ }
+}
+
+func TestResolveAcceptsBareCoordinates(t *testing.T) {
+ stubGeocoder(t, nil)
+ got, err := resolve(tmpStore(t), "50.8367,16.6467", 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if got.Lat != 50.8367 || got.Lon != 16.6467 {
+ t.Errorf("got %+v, want the coordinates parsed as given", got)
+ }
+}
+
+// The listing is the whole remedy: if it omits the region the user cannot tell
+// the duplicates apart, and if it omits coordinates there is no way to reach a
+// candidate that -pick is not being used for.
+func TestAmbiguousListingIsActionable(t *testing.T) {
+ out := ambiguousListing(&ambiguousError{place: "Wiry, PL", candidates: twoWirys()})
+ for _, want := range []string{
+ "1", "2",
+ "Greater Poland", "Lower Silesia",
+ "52.3205", "50.8367",
+ "-pick",
+ } {
+ if !strings.Contains(out, want) {
+ t.Errorf("listing is missing %q; user cannot act on it:\n%s", want, out)
+ }
+ }
+}