Bound Weather API response diagnostics
This commit is contained in:
@@ -201,8 +201,9 @@ func TestFetchBundleRecordsSourceHash(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestHTTPErrorIsActionable(t *testing.T) {
|
||||
const marker = "upstream-secret-marker"
|
||||
server := fixtureServer(t, map[string]handlerOverride{
|
||||
"/forecast/hourly": {status: http.StatusBadGateway, body: `upstream failed`},
|
||||
"/forecast/hourly": {status: http.StatusBadGateway, body: marker + strings.Repeat("x", 4096)},
|
||||
}, nil)
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
|
||||
@@ -213,6 +214,9 @@ func TestHTTPErrorIsActionable(t *testing.T) {
|
||||
if !strings.Contains(err.Error(), "/forecast/hourly") || !strings.Contains(err.Error(), "502") {
|
||||
t.Fatalf("error = %q, want endpoint and status", err.Error())
|
||||
}
|
||||
if strings.Contains(err.Error(), marker) {
|
||||
t.Fatalf("error = %q, must not contain upstream response text", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupRetriesBeforeFetchBundle(t *testing.T) {
|
||||
@@ -291,6 +295,92 @@ func TestWarmupDoesNotRetryPermanentStatus(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupErrorDiagnosticsRedactResponseBody(t *testing.T) {
|
||||
const marker = "upstream-secret-marker"
|
||||
server := fixtureServer(t, map[string]handlerOverride{
|
||||
defaultWarmupEndpoint: {status: http.StatusNotFound, body: marker + strings.Repeat("x", 4096)},
|
||||
}, nil)
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
|
||||
_, err := client.FetchBundle(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("FetchBundle() error = nil, want warmup error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), defaultWarmupEndpoint) || !strings.Contains(err.Error(), "404") {
|
||||
t.Fatalf("error = %q, want warmup endpoint and status", err.Error())
|
||||
}
|
||||
if strings.Contains(err.Error(), marker) {
|
||||
t.Fatalf("error = %q, must not contain upstream response text", err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchAcceptsResponseAtBodyLimit(t *testing.T) {
|
||||
body := paddedJSON(t, `{"data":null}`, int(maxResponseBodyBytes))
|
||||
server := fixtureServer(t, map[string]handlerOverride{
|
||||
"/forecast/narrative": {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(body))
|
||||
}},
|
||||
}, nil)
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
|
||||
if _, err := client.FetchBundle(context.Background()); err != nil {
|
||||
t.Fatalf("FetchBundle() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRejectsOversizedResponseWithoutRetry(t *testing.T) {
|
||||
var requested []string
|
||||
var narrativeCalls int
|
||||
oversizedBody := paddedJSON(t, `{"data":null}`, int(maxResponseBodyBytes)) + "x"
|
||||
server := fixtureServer(t, map[string]handlerOverride{
|
||||
"/forecast/narrative": {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
narrativeCalls++
|
||||
_, _ = w.Write([]byte(oversizedBody))
|
||||
}},
|
||||
}, &requested)
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
|
||||
_, err := client.FetchBundle(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("FetchBundle() error = nil, want oversized response error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "/forecast/narrative") || !strings.Contains(err.Error(), errResponseBodyTooLarge.Error()) {
|
||||
t.Fatalf("error = %q, want endpoint and response limit", err.Error())
|
||||
}
|
||||
if narrativeCalls != 1 {
|
||||
t.Fatalf("narrative calls = %d, want no retry", narrativeCalls)
|
||||
}
|
||||
if containsPath(requested, "/alerts/active") {
|
||||
t.Fatalf("requested paths = %v, want source failure before later fetches", requested)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWarmupRejectsOversizedResponseWithoutRetry(t *testing.T) {
|
||||
var requested []string
|
||||
oversizedBody := paddedJSON(t, `{"data":{}}`, int(maxResponseBodyBytes)) + "x"
|
||||
server := fixtureServer(t, map[string]handlerOverride{
|
||||
defaultWarmupEndpoint: {handler: func(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write([]byte(oversizedBody))
|
||||
}},
|
||||
}, &requested)
|
||||
client := newTestClient(t, server.URL+"/", nil)
|
||||
client.warmupAttempts = 2
|
||||
|
||||
_, err := client.FetchBundle(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("FetchBundle() error = nil, want oversized warmup response error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), defaultWarmupEndpoint) || !strings.Contains(err.Error(), errResponseBodyTooLarge.Error()) {
|
||||
t.Fatalf("error = %q, want warmup endpoint and response limit", err.Error())
|
||||
}
|
||||
if got := countPath(requested, defaultWarmupEndpoint); got != 1 {
|
||||
t.Fatalf("warmup requests = %d, want no retry; all requests = %v", got, requested)
|
||||
}
|
||||
if containsPath(requested, "/observations") {
|
||||
t.Fatalf("requested paths = %v, want warmup failure before source fetches", requested)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFetchRetriesRetryableStatus(t *testing.T) {
|
||||
var hourlyCalls int
|
||||
server := fixtureServer(t, map[string]handlerOverride{
|
||||
@@ -766,6 +856,14 @@ func fixedNow() time.Time {
|
||||
return time.Date(2026, 5, 29, 15, 0, 0, 0, time.UTC)
|
||||
}
|
||||
|
||||
func paddedJSON(t *testing.T, value string, size int) string {
|
||||
t.Helper()
|
||||
if len(value) > size {
|
||||
t.Fatalf("JSON value length = %d, exceeds requested size %d", len(value), size)
|
||||
}
|
||||
return value + strings.Repeat(" ", size-len(value))
|
||||
}
|
||||
|
||||
func containsPath(requested []string, path string) bool {
|
||||
for _, rawURL := range requested {
|
||||
if strings.HasPrefix(rawURL, path+"?") || rawURL == path {
|
||||
|
||||
Reference in New Issue
Block a user