Zwei Schalter, unabhaengig voneinander: Schreibtisch, Leiste, beides oder keins. Die Stadt wird einmal gesucht und mit ihren Koordinaten gespeichert, sonst suchte jede Abfrage den Ort erneut. Geholt wird serverseitig und zehn Minuten zwischengespeichert. Der Grund ist nicht Sparsamkeit: Die Leiste steht auf jeder Seite jeder App, und fragte sie selbst an, ginge bei jedem Seitenaufruf eine Anfrage samt IP an einen Fremden — und die CSP jeder App muesste ihn erlauben. Kein Ort eingestellt oder Dienst nicht erreichbar: 204, und das Symbol bleibt weg. Eine Zahl von gestern waere schlechter als keine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
151 lines
4.5 KiB
Go
151 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
)
|
|
|
|
// Ein erfundener Wetterdienst. Kein Test in diesem Projekt fragt je wirklich
|
|
// bei Open-Meteo an: Das wäre langsam, unzuverlässig und würde einen fremden
|
|
// Dienst für unsere Testläufe benutzen.
|
|
func fakeWetter(t *testing.T, code int, grad float64, aufrufe *int32) *wetterDienst {
|
|
t.Helper()
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if aufrufe != nil {
|
|
atomic.AddInt32(aufrufe, 1)
|
|
}
|
|
if strings.Contains(r.URL.Path, "search") {
|
|
json.NewEncoder(w).Encode(map[string]any{"results": []map[string]any{
|
|
{"name": "Köln", "country": "Deutschland", "admin1": "Nordrhein-Westfalen",
|
|
"latitude": 50.938, "longitude": 6.96},
|
|
}})
|
|
return
|
|
}
|
|
json.NewEncoder(w).Encode(map[string]any{"current": map[string]any{
|
|
"temperature_2m": grad, "weather_code": code,
|
|
}})
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
d := neuerWetterdienst()
|
|
d.wetterURL = srv.URL + "/forecast"
|
|
d.sucheURL = srv.URL + "/search"
|
|
return d
|
|
}
|
|
|
|
func TestWetterKommtAn(t *testing.T) {
|
|
d := fakeWetter(t, 61, 12.4, nil)
|
|
w, err := d.Jetzt(context.Background(), "Köln", 50.9, 6.9)
|
|
if err != nil {
|
|
t.Fatalf("unerwartet: %v", err)
|
|
}
|
|
if w.Ort != "Köln" || w.Grad != 12.4 {
|
|
t.Fatalf("falsche Werte: %+v", w)
|
|
}
|
|
if w.Glyph != "regen" || w.Text != "regen" {
|
|
t.Fatalf("Code 61 ist Regen, nicht %q/%q", w.Glyph, w.Text)
|
|
}
|
|
}
|
|
|
|
// Der Zwischenspeicher ist der Grund, warum die Leiste auf jeder Seite jeder App
|
|
// stehen darf: Ohne ihn ginge bei jedem Seitenaufruf eine Anfrage nach draußen.
|
|
func TestZweiteFrageGehtNichtNachDraussen(t *testing.T) {
|
|
var aufrufe int32
|
|
d := fakeWetter(t, 0, 21, &aufrufe)
|
|
for i := 0; i < 5; i++ {
|
|
if _, err := d.Jetzt(context.Background(), "Köln", 50.9, 6.9); err != nil {
|
|
t.Fatalf("unerwartet: %v", err)
|
|
}
|
|
}
|
|
if aufrufe != 1 {
|
|
t.Fatalf("%d Anfragen nach draußen, erwartet 1", aufrufe)
|
|
}
|
|
}
|
|
|
|
func TestAndererOrtWirdGetrenntGehalten(t *testing.T) {
|
|
var aufrufe int32
|
|
d := fakeWetter(t, 0, 21, &aufrufe)
|
|
d.Jetzt(context.Background(), "Köln", 50.9, 6.9)
|
|
d.Jetzt(context.Background(), "Wien", 48.2, 16.4)
|
|
if aufrufe != 2 {
|
|
t.Fatalf("zwei Orte, %d Anfragen", aufrufe)
|
|
}
|
|
}
|
|
|
|
func TestWettercodesWerdenZuSymbolen(t *testing.T) {
|
|
fälle := map[int]string{
|
|
0: "sonne", 2: "heiter", 3: "wolke", 45: "nebel",
|
|
61: "regen", 80: "regen", 73: "schnee", 86: "schnee", 95: "gewitter",
|
|
}
|
|
for code, will := range fälle {
|
|
if g, _ := wmo(code); g != will {
|
|
t.Errorf("Code %d → %q, erwartet %q", code, g, will)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOrtssucheGibtKoordinatenZurueck(t *testing.T) {
|
|
d := fakeWetter(t, 0, 20, nil)
|
|
orte, err := d.Suche(context.Background(), "Köln")
|
|
if err != nil {
|
|
t.Fatalf("unerwartet: %v", err)
|
|
}
|
|
if len(orte) != 1 || orte[0].Lat == 0 || orte[0].Lon == 0 {
|
|
t.Fatalf("ohne Koordinaten müsste jede Abfrage neu suchen: %+v", orte)
|
|
}
|
|
// Bundesland statt Land: „Köln, Deutschland" gibt es mehrfach.
|
|
if !strings.Contains(orte[0].Land, "Nordrhein-Westfalen") {
|
|
t.Fatalf("Land zu grob: %q", orte[0].Land)
|
|
}
|
|
}
|
|
|
|
func TestSucheOhneBegriffFragtNichtAn(t *testing.T) {
|
|
var aufrufe int32
|
|
d := fakeWetter(t, 0, 20, &aufrufe)
|
|
if _, err := d.Suche(context.Background(), " "); err == nil {
|
|
t.Fatal("leerer Begriff müsste abgelehnt werden")
|
|
}
|
|
if aufrufe != 0 {
|
|
t.Fatalf("leerer Begriff ging trotzdem nach draußen (%d)", aufrufe)
|
|
}
|
|
}
|
|
|
|
// Ohne eingestellten Ort ist 204 die richtige Antwort: kein Fehler, nur nichts
|
|
// zu zeigen. Die Leiste lässt das Symbol dann weg, statt etwas zu melden.
|
|
func TestOhneOrtGibtEsKeinWetter(t *testing.T) {
|
|
s := newTestServer(t, "") // ohne Anbieter: offener Betrieb, kein Anmelden nötig
|
|
s.wetter = fakeWetter(t, 0, 20, nil)
|
|
|
|
rec := httptest.NewRecorder()
|
|
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/api/weather", nil))
|
|
if rec.Code != http.StatusNoContent {
|
|
t.Fatalf("ohne Ort erwartet 204, bekam %d", rec.Code)
|
|
}
|
|
}
|
|
|
|
func TestMitOrtKommtDasWetter(t *testing.T) {
|
|
s := newTestServer(t, "")
|
|
s.wetter = fakeWetter(t, 3, 8.6, nil)
|
|
if err := s.store.SetSettings(localSub, map[string]string{
|
|
"weatherCity": "Köln", "weatherLat": "50.938", "weatherLon": "6.96",
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
rec := httptest.NewRecorder()
|
|
s.routes().ServeHTTP(rec, httptest.NewRequest("GET", "/api/weather", nil))
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("erwartet 200, bekam %d: %s", rec.Code, rec.Body)
|
|
}
|
|
var w Wetter
|
|
json.Unmarshal(rec.Body.Bytes(), &w)
|
|
if w.Ort != "Köln" || w.Glyph != "wolke" {
|
|
t.Fatalf("falsches Wetter: %+v", w)
|
|
}
|
|
}
|