196 lines
5.4 KiB
Go
196 lines
5.4 KiB
Go
package httpapi
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"text/template"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/ejr/feedapi/endpoint"
|
|
apierrors "gitea.maximumdirect.net/ejr/feedapi/errors"
|
|
"gitea.maximumdirect.net/ejr/feedapi/render"
|
|
"gitea.maximumdirect.net/ejr/feedapi/templates"
|
|
"gitea.maximumdirect.net/ejr/feedapi/transport/httpx"
|
|
"gitea.maximumdirect.net/ejr/weatherfeeder/model"
|
|
)
|
|
|
|
type fakeService struct {
|
|
observation *model.WeatherObservation
|
|
forecast *model.WeatherForecastRun
|
|
alerts *model.WeatherAlertRun
|
|
err error
|
|
}
|
|
|
|
func (s *fakeService) LatestObservation(context.Context) (*model.WeatherObservation, error) {
|
|
return s.observation, s.err
|
|
}
|
|
|
|
func (s *fakeService) LatestHourlyForecast(context.Context) (*model.WeatherForecastRun, error) {
|
|
return s.forecast, s.err
|
|
}
|
|
|
|
func (s *fakeService) LatestActiveAlerts(context.Context) (*model.WeatherAlertRun, error) {
|
|
return s.alerts, s.err
|
|
}
|
|
|
|
func TestObservationsRejectUnknownQueryParameter(t *testing.T) {
|
|
h := newHandler(t, &fakeService{}, "/observations")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/observations?bogus=1", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", w.Code)
|
|
}
|
|
|
|
var env apierrors.Envelope
|
|
if err := json.Unmarshal(w.Body.Bytes(), &env); err != nil {
|
|
t.Fatalf("decode error envelope: %v", err)
|
|
}
|
|
if env.Error == nil || env.Error.Code != apierrors.CodeInvalidParameter {
|
|
t.Fatalf("expected invalid_parameter code, got %+v", env.Error)
|
|
}
|
|
}
|
|
|
|
func TestObservationsNoDataReturnsNullEnvelopeData(t *testing.T) {
|
|
h := newHandler(t, &fakeService{}, "/observations")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/observations", 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 TestObservationsPopulatedJSONEnvelope(t *testing.T) {
|
|
now := time.Date(2026, 3, 19, 18, 0, 0, 0, time.UTC)
|
|
h := newHandler(t, &fakeService{
|
|
observation: &model.WeatherObservation{
|
|
StationID: "KSTL",
|
|
StationName: "St. Louis",
|
|
Timestamp: now,
|
|
},
|
|
}, "/observations")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/observations", nil)
|
|
h.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", w.Code)
|
|
}
|
|
|
|
var payload struct {
|
|
Data struct {
|
|
StationID string `json:"stationId"`
|
|
} `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("decode envelope: %v", err)
|
|
}
|
|
if payload.Data.StationID != "KSTL" {
|
|
t.Fatalf("expected stationId KSTL, got %q", payload.Data.StationID)
|
|
}
|
|
}
|
|
|
|
func TestFormatNegotiationXMLAndText(t *testing.T) {
|
|
hXML := newHandler(t, &fakeService{alerts: &model.WeatherAlertRun{AsOf: time.Now().UTC()}}, "/alerts/active")
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/alerts/active", nil)
|
|
req.Header.Set("Accept", "application/xml")
|
|
hXML.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200 for xml request, 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"))
|
|
}
|
|
|
|
hText := newHandler(t, &fakeService{forecast: &model.WeatherForecastRun{Product: model.ForecastProductHourly}}, "/forecast/hourly")
|
|
w = httptest.NewRecorder()
|
|
req = httptest.NewRequest(http.MethodGet, "/forecast/hourly?format=text", nil)
|
|
hText.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(), "Forecast text") {
|
|
t.Fatalf("expected rendered text template body, got %q", w.Body.String())
|
|
}
|
|
}
|
|
|
|
func newHandler(t *testing.T, svc Service, path string) http.Handler {
|
|
t.Helper()
|
|
|
|
def := definitionForPath(t, Definitions(svc), path)
|
|
return httpx.Adapt(def, httpx.Dependencies{
|
|
Renderers: testRenderers(t),
|
|
DefaultFormat: render.FormatJSON,
|
|
})
|
|
}
|
|
|
|
func definitionForPath(t *testing.T, defs []endpoint.Definition, path string) endpoint.Definition {
|
|
t.Helper()
|
|
for _, def := range defs {
|
|
if def.Path == path {
|
|
return def
|
|
}
|
|
}
|
|
t.Fatalf("endpoint not found: %s", path)
|
|
return endpoint.Definition{}
|
|
}
|
|
|
|
func testRenderers(t *testing.T) *render.Registry {
|
|
t.Helper()
|
|
|
|
reg := render.NewRegistry()
|
|
if err := reg.Register(render.NewJSONRenderer()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := reg.Register(render.NewXMLRenderer()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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",
|
|
} {
|
|
tmpl, err := template.New(name).Parse(body)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := tmplReg.Register(name, tmpl); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
if err := reg.Register(templates.NewRenderer(tmplReg)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
return reg
|
|
}
|