From 2081e91caefda0c5dd92069401eee4ffc2e075dd Mon Sep 17 00:00:00 2001 From: siddharth ravikumar Date: Sun, 5 Jun 2022 14:47:11 -0400 Subject: nws: add get A thin HTTP GET wrapper for hitting NWS API endpoints. --- nws/nws.go | 55 ++++++++++++++++++++++++++++++++++ nws/nws_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 146 insertions(+), 1 deletion(-) diff --git a/nws/nws.go b/nws/nws.go index 8c28f19..2fc5662 100644 --- a/nws/nws.go +++ b/nws/nws.go @@ -8,6 +8,7 @@ import ( "encoding/json" "fmt" "io" + "time" "ricketyspace.net/peach/client" ) @@ -198,3 +199,57 @@ func GetForecastHourly(point *Point) (*Forecast, error) { } return forecast, nil } + +// HTTP GET a NWS endpoint. +func get(url string) ([]byte, *Error) { + tries := 5 + retryDelay := 100 * time.Millisecond + for { + resp, err := client.Get(url) + if err != nil { + return nil, &Error{ + Title: fmt.Sprintf("http get failed: %v", url), + Type: "http-get", + Status: 500, + Detail: err.Error(), + } + } + if tries > 0 && resp.StatusCode != 200 { + tries -= 1 + + // Wait before re-try. + time.Sleep(retryDelay) + + retryDelay *= 2 // Exponential back-off delay. + continue // Re-try + } + + // Parse response body. + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, &Error{ + Title: fmt.Sprintf("parsing body: %v", url), + Type: "response-body", + Status: 500, + Detail: err.Error(), + } + } + + // Check if the request failed. + if resp.StatusCode != 200 { + nwsErr := Error{} + err := json.Unmarshal(body, &nwsErr) + if err != nil { + return nil, &Error{ + Title: fmt.Sprintf("json decode: %v", url), + Type: "json-decode", + Status: 500, + Detail: err.Error(), + } + } + return nil, &nwsErr + } + // Response OK. + return body, nil + } +} diff --git a/nws/nws_test.go b/nws/nws_test.go index ffa05c8..bac3d83 100644 --- a/nws/nws_test.go +++ b/nws/nws_test.go @@ -3,7 +3,13 @@ package nws -import "testing" +import ( + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "testing" +) func TestPoints(t *testing.T) { // Test valid lat,lng. @@ -142,3 +148,87 @@ func TestGetForecastHourly(t *testing.T) { } } } + +func TestNWSGetWrapper(t *testing.T) { + // Initialize test NWS server. + fails := 0 + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if fails > 0 { + fails -= 1 + http.Error(w, `{"type":"urn:noaa:nws:api:UnexpectedProblem","title":"Unexpected Problem","status":500,"detail":"An unexpected problem has occurred.","instance":"urn:noaa:nws:api:request:493c3a1d-f87e-407f-ae2c-24483f5aab63","correlationId":"493c3a1d-f87e-407f-ae2c-24483f5aab63","additionalProp1":{}}`, 500) + return + } + // Success. + fmt.Fprintln(w, `{"@context":[],"properties":{"gridId":"CLE","gridX":82,"gridY":64,"forecast":"https://api.weather.gov/gridpoints/CLE/82,64/forecast","forecastHourly":"https://api.weather.gov/gridpoints/CLE/82,64/forecast/hourly","relativeLocation":{"properties":{"city":"Cleveland","state":"OH"}}}}`) + })) + defer ts.Close() + + // Test 1 - Server fails 5 times. + fails = 5 + _, err := get(ts.URL) + if err != nil { + t.Errorf("get failed: %v", err) + return + } + + // Test 2 - Server fails 6 times. + fails = 6 + respBody, err := get(ts.URL) + if err == nil { + t.Errorf("get did not fail: %s", respBody) + return + } + if err != nil && respBody != nil { + t.Errorf("body is not nil: %s", respBody) + } + if err.Title != "Unexpected Problem" { + t.Errorf("err title: %s", err.Title) + return + } + if err.Type != "urn:noaa:nws:api:UnexpectedProblem" { + t.Errorf("err type: %s", err.Type) + return + } + if err.Status != 500 { + t.Errorf("err status: %d", err.Status) + return + } + if err.Detail != "An unexpected problem has occurred." { + t.Errorf("err detail: %s", err.Detail) + return + } + + // Test 3 - Server fails 1 time. + fails = 1 + respBody, err = get(ts.URL) + if err != nil { + t.Errorf("get failed: %v", err) + return + } + if respBody == nil { + t.Errorf("body: %s", respBody) + return + } + point := new(Point) + jerr := json.Unmarshal(respBody, point) + if jerr != nil { + t.Errorf("points: decode: %v", jerr) + return + } + if point.Properties.Forecast == "" { + t.Errorf("points: forecast empty") + return + } + if point.Properties.ForecastHourly == "" { + t.Errorf("points: forecasthourly empty") + return + } + if point.Properties.RelativeLocation.Properties.City == "" { + t.Errorf("points: city empty") + return + } + if point.Properties.RelativeLocation.Properties.State == "" { + t.Errorf("points: state empty") + return + } +} -- cgit v1.2.3