Give every API value with a unit as a number (closes #199)
check / check (push) Canceled after 0s
check / check (push) Canceled after 0s
A value with a unit is now a number, and its field name ends in the unit, as the vendored API_POLICIES.md requires. @meta.execution_time becomes execution_time_ms. In the stats answers, uptime, connection_duration, memory_usage and whois_stats.current_interval become uptime_seconds, connection_duration_seconds, memory_usage_bytes and whois_stats.current_interval_seconds. connection_duration_seconds is null while the feed is not connected. The status page formats the numbers itself. REPO_POLICIES.md is re-vendored from the same sneak/prompts commit. Model: opus-5-5
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"net/http/httptest"
|
||||
"net/netip"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -100,6 +101,81 @@ func TestJSONValidationKeeps4xxWithoutBody(t *testing.T) {
|
||||
checkJSONErrorMessage(t, name, rec, http.StatusText(http.StatusBadRequest))
|
||||
}
|
||||
|
||||
// TestJSONAnswersPutNoUnitInAString sends a request to each JSON route and
|
||||
// checks each answer. Its @meta.execution_time_ms is a number, and none of its
|
||||
// strings is a number with a unit, such as "12 ms", "1.1 GB" or "1h18m34s"
|
||||
// (https://git.eeqj.de/sneak/routewatch/issues/199). The health check answers
|
||||
// 503, since the stream is not connected, and the last path matches no route.
|
||||
func TestJSONAnswersPutNoUnitInAString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
numberWithUnit := regexp.MustCompile(
|
||||
`^([0-9]+(\.[0-9]+)? ?(ns|µs|ms|s|m|h|B|kB|MB|GB|TB))+$`)
|
||||
|
||||
s := newServerWithTestRoutes(t)
|
||||
s.resolver = &fakeResolver{}
|
||||
s.SetASNFetcher(fixedIntervalASNFetcher{interval: time.Second})
|
||||
|
||||
paths := []string{
|
||||
statusJSONPath,
|
||||
statsPath,
|
||||
"/.well-known/healthcheck.json",
|
||||
"/api/v1/ip/" + untrustedPeerAddr,
|
||||
"/ip/" + untrustedPeerAddr,
|
||||
asJSONPath,
|
||||
"/api/v1/prefix/" + testPrefixV4,
|
||||
"/api/v1/search/as?q=google",
|
||||
"/api/v1/no-such-route",
|
||||
}
|
||||
|
||||
for _, path := range paths {
|
||||
rec := httptest.NewRecorder()
|
||||
s.router.ServeHTTP(rec, httptest.NewRequestWithContext(
|
||||
t.Context(), http.MethodGet, path, nil))
|
||||
|
||||
var answer map[string]any
|
||||
|
||||
err := json.Unmarshal(rec.Body.Bytes(), &answer)
|
||||
if err != nil {
|
||||
t.Errorf("%s: decoding the answer: %v; answer %s", path, err, rec.Body)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
meta, _ := answer["@meta"].(map[string]any)
|
||||
if _, ok := meta["execution_time_ms"].(float64); !ok {
|
||||
t.Errorf("%s: @meta.execution_time_ms is not a number: %s",
|
||||
path, rec.Body)
|
||||
}
|
||||
|
||||
for _, value := range stringsIn(answer) {
|
||||
if numberWithUnit.MatchString(value) {
|
||||
t.Errorf("%s: %q is a number with a unit in a string", path, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stringsIn returns every string in a decoded JSON value, at any depth.
|
||||
func stringsIn(value any) []string {
|
||||
var found []string
|
||||
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
found = append(found, v)
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
found = append(found, stringsIn(item)...)
|
||||
}
|
||||
case map[string]any:
|
||||
for _, item := range v {
|
||||
found = append(found, stringsIn(item)...)
|
||||
}
|
||||
}
|
||||
|
||||
return found
|
||||
}
|
||||
|
||||
// TestPagesCarryTheCSPNonce checks that every script and style block of each
|
||||
// page carries the nonce its Content-Security-Policy header names, so that the
|
||||
// browser runs and applies them, that each answer has a nonce of its own, and
|
||||
|
||||
Reference in New Issue
Block a user