Add a /conditions/current endpoint
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
All checks were successful
ci/woodpecker/push/build-image Pipeline was successful
This commit is contained in:
@@ -18,6 +18,7 @@ type Service interface {
|
||||
LatestObservation(ctx context.Context) (*model.WeatherObservation, error)
|
||||
LatestHourlyForecast(ctx context.Context) (*model.WeatherForecastRun, error)
|
||||
LatestActiveAlerts(ctx context.Context) (*model.WeatherAlertRun, error)
|
||||
CurrentConditions(ctx context.Context) (*core.CurrentConditions, error)
|
||||
}
|
||||
|
||||
type queryRequest struct {
|
||||
@@ -65,6 +66,19 @@ func Definitions(svc Service) []endpoint.Definition {
|
||||
endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText),
|
||||
endpoint.WithTemplate("alerts_active.txt.tmpl"),
|
||||
),
|
||||
endpoint.GET(
|
||||
"/conditions/current",
|
||||
bindQuery,
|
||||
func(ctx context.Context, req queryRequest) (any, error) {
|
||||
conditions, err := svc.CurrentConditions(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response.Envelope{Data: core.CurrentConditionsPayload(conditions, req.Units)}, nil
|
||||
},
|
||||
endpoint.WithProduces(render.FormatJSON, render.FormatXML, render.FormatText),
|
||||
endpoint.WithTemplate("conditions_current.txt.tmpl"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"gitea.maximumdirect.net/ejr/feedapi/render"
|
||||
"gitea.maximumdirect.net/ejr/feedapi/templates"
|
||||
"gitea.maximumdirect.net/ejr/feedapi/transport/httpx"
|
||||
"gitea.maximumdirect.net/ejr/weatherapi/internal/core"
|
||||
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
||||
)
|
||||
|
||||
@@ -23,6 +24,7 @@ type fakeService struct {
|
||||
observation *model.WeatherObservation
|
||||
forecast *model.WeatherForecastRun
|
||||
alerts *model.WeatherAlertRun
|
||||
conditions *core.CurrentConditions
|
||||
err error
|
||||
}
|
||||
|
||||
@@ -38,6 +40,10 @@ func (s *fakeService) LatestActiveAlerts(context.Context) (*model.WeatherAlertRu
|
||||
return s.alerts, s.err
|
||||
}
|
||||
|
||||
func (s *fakeService) CurrentConditions(context.Context) (*core.CurrentConditions, error) {
|
||||
return s.conditions, s.err
|
||||
}
|
||||
|
||||
func TestObservationsRejectUnknownQueryParameter(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{}, "/observations")
|
||||
|
||||
@@ -216,6 +222,218 @@ func TestAlertsUSUnitsKeepSchema(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestObservationUSUnitsWithXMLFormat(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
observation: &model.WeatherObservation{
|
||||
StationID: "KSTL",
|
||||
Timestamp: time.Now().UTC(),
|
||||
ConditionCode: 1,
|
||||
TemperatureC: float64Ptr(20),
|
||||
},
|
||||
}, "/observations")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/observations?format=xml&units=us", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Header().Get("Content-Type"), "application/xml") {
|
||||
t.Fatalf("expected xml content type, got %q", w.Header().Get("Content-Type"))
|
||||
}
|
||||
if strings.Contains(w.Body.String(), "temperatureC") {
|
||||
t.Fatalf("expected metric field temperatureC to be omitted in XML payload: %s", w.Body.String())
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "temperatureF") {
|
||||
t.Fatalf("expected US field temperatureF in XML payload: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestForecastUSUnitsWithXMLFormatUppercaseQuery(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
forecast: &model.WeatherForecastRun{
|
||||
Product: model.ForecastProductHourly,
|
||||
IssuedAt: time.Now().UTC(),
|
||||
Periods: []model.WeatherForecastPeriod{{
|
||||
StartTime: time.Now().UTC(),
|
||||
EndTime: time.Now().UTC().Add(time.Hour),
|
||||
ConditionCode: 1,
|
||||
TemperatureC: float64Ptr(10),
|
||||
}},
|
||||
},
|
||||
}, "/forecast/hourly")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/forecast/hourly?format=XML&units=US", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Header().Get("Content-Type"), "application/xml") {
|
||||
t.Fatalf("expected xml content type, got %q", w.Header().Get("Content-Type"))
|
||||
}
|
||||
body := w.Body.String()
|
||||
if strings.Contains(body, "temperatureC") {
|
||||
t.Fatalf("expected metric field temperatureC to be omitted in XML payload: %s", body)
|
||||
}
|
||||
if !strings.Contains(body, "temperatureF") {
|
||||
t.Fatalf("expected US field temperatureF in XML payload: %s", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentConditionsNoDataReturnsNullEnvelopeData(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{}, "/conditions/current")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/conditions/current", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data *json.RawMessage `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode envelope: %v", err)
|
||||
}
|
||||
if payload.Data != nil {
|
||||
t.Fatalf("expected data null, got %s", string(*payload.Data))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentConditionsMetricDefaultJSON(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
conditions: &core.CurrentConditions{
|
||||
TemperatureC: float64Ptr(10),
|
||||
ApparentTemperatureC: float64Ptr(9),
|
||||
DewpointC: float64Ptr(5),
|
||||
RelativeHumidityPercent: float64Ptr(75),
|
||||
WindSpeedKmh: float64Ptr(18),
|
||||
WindDirectionDegrees: float64Ptr(135),
|
||||
ConditionCode: 63,
|
||||
},
|
||||
}, "/conditions/current")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/conditions/current", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode envelope: %v", err)
|
||||
}
|
||||
if _, ok := payload.Data["temperatureC"]; !ok {
|
||||
t.Fatalf("expected temperatureC in metric payload")
|
||||
}
|
||||
if _, ok := payload.Data["temperatureF"]; ok {
|
||||
t.Fatalf("expected temperatureF omitted in metric payload")
|
||||
}
|
||||
if payload.Data["conditionText"] != "Rain" {
|
||||
t.Fatalf("expected conditionText Rain, got %#v", payload.Data["conditionText"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentConditionsUSJSON(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
conditions: &core.CurrentConditions{
|
||||
TemperatureC: float64Ptr(10),
|
||||
ApparentTemperatureC: float64Ptr(9),
|
||||
DewpointC: float64Ptr(5),
|
||||
RelativeHumidityPercent: float64Ptr(75),
|
||||
WindSpeedKmh: float64Ptr(18),
|
||||
WindDirectionDegrees: float64Ptr(135),
|
||||
ConditionCode: 63,
|
||||
},
|
||||
}, "/conditions/current")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/conditions/current?units=us", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
var payload struct {
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode envelope: %v", err)
|
||||
}
|
||||
if _, ok := payload.Data["temperatureF"]; !ok {
|
||||
t.Fatalf("expected temperatureF in us payload")
|
||||
}
|
||||
if _, ok := payload.Data["temperatureC"]; ok {
|
||||
t.Fatalf("expected temperatureC omitted in us payload")
|
||||
}
|
||||
if _, ok := payload.Data["windSpeedMph"]; !ok {
|
||||
t.Fatalf("expected windSpeedMph in us payload")
|
||||
}
|
||||
if _, ok := payload.Data["windSpeedKmh"]; ok {
|
||||
t.Fatalf("expected windSpeedKmh omitted in us payload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentConditionsXMLAndTextFormats(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{
|
||||
conditions: &core.CurrentConditions{
|
||||
TemperatureC: float64Ptr(10),
|
||||
WindSpeedKmh: float64Ptr(18),
|
||||
ConditionCode: 2,
|
||||
},
|
||||
}, "/conditions/current")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/conditions/current?format=xml&units=us", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Header().Get("Content-Type"), "application/xml") {
|
||||
t.Fatalf("expected xml content type, got %q", w.Header().Get("Content-Type"))
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "temperatureF") {
|
||||
t.Fatalf("expected US field temperatureF in XML payload: %s", w.Body.String())
|
||||
}
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req = httptest.NewRequest(http.MethodGet, "/conditions/current?format=TEXT", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 for text request, got %d", w.Code)
|
||||
}
|
||||
if !strings.Contains(w.Header().Get("Content-Type"), "text/plain") {
|
||||
t.Fatalf("expected text/plain content type, got %q", w.Header().Get("Content-Type"))
|
||||
}
|
||||
if !strings.Contains(w.Body.String(), "Conditions text") {
|
||||
t.Fatalf("expected rendered text template body, got %q", w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCurrentConditionsRejectUnknownQueryParameter(t *testing.T) {
|
||||
h := newHandler(t, &fakeService{}, "/conditions/current")
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req := httptest.NewRequest(http.MethodGet, "/conditions/current?bogus=1", nil)
|
||||
h.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Fatalf("expected 400, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func newHandler(t *testing.T, svc Service, path string) http.Handler {
|
||||
t.Helper()
|
||||
|
||||
@@ -250,9 +468,10 @@ func testRenderers(t *testing.T) *render.Registry {
|
||||
|
||||
tmplReg := templates.NewRegistry()
|
||||
for name, body := range map[string]string{
|
||||
"observations.txt.tmpl": "Observation text",
|
||||
"forecast_hourly.txt.tmpl": "Forecast text",
|
||||
"alerts_active.txt.tmpl": "Alerts text",
|
||||
"observations.txt.tmpl": "Observation text",
|
||||
"forecast_hourly.txt.tmpl": "Forecast text",
|
||||
"alerts_active.txt.tmpl": "Alerts text",
|
||||
"conditions_current.txt.tmpl": "Conditions text",
|
||||
} {
|
||||
tmpl, err := template.New(name).Parse(body)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user