Public source validation / validate (push) Failing after 3m8s
223 lines
7.8 KiB
Go
223 lines
7.8 KiB
Go
package live_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
"github.com/coder/websocket/wsjson"
|
|
"github.com/itworx/pulse/internal/auth"
|
|
"github.com/itworx/pulse/internal/live"
|
|
"github.com/itworx/pulse/internal/metriccatalog"
|
|
"github.com/itworx/pulse/internal/queryplan"
|
|
)
|
|
|
|
type sampler struct{}
|
|
|
|
func (sampler) Sample(_ context.Context, request queryplan.Request) ([]live.Sample, error) {
|
|
value := float64(request.MaxPoints)
|
|
return []live.Sample{{Series: "test", Timestamp: time.Now().UTC(), Value: &value, Freshness: "fresh"}}, nil
|
|
}
|
|
|
|
func plannerForTest(t *testing.T) *queryplan.Planner {
|
|
t.Helper()
|
|
registry, err := metriccatalog.DefaultRegistry()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
planner := queryplan.NewPlanner(registry, queryplan.Limits{})
|
|
return &planner
|
|
}
|
|
|
|
func validQuery() map[string]any {
|
|
now := time.Now().UTC().Truncate(time.Second)
|
|
return map[string]any{
|
|
"metric": "container.cpu.utilization",
|
|
"scope": map[string]string{"containerId": "media_server"},
|
|
"range": map[string]any{"from": now.Add(-time.Minute).Format(time.RFC3339), "to": now.Format(time.RFC3339), "stepSeconds": 15},
|
|
"aggregation": "avg",
|
|
"maxSeries": 1,
|
|
"maxPoints": 60,
|
|
}
|
|
}
|
|
|
|
func authorizedServer(t *testing.T, handler live.Handler) *httptest.Server {
|
|
t.Helper()
|
|
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
r = r.WithContext(auth.WithPrincipal(r.Context(), auth.Principal{Subject: "viewer", Role: auth.RoleViewer}))
|
|
handler.ServeHTTP(w, r)
|
|
}))
|
|
}
|
|
|
|
func TestHandlerRejectsUnauthenticatedBeforeUpgrade(t *testing.T) {
|
|
server := httptest.NewServer(live.Handler{})
|
|
defer server.Close()
|
|
_, response, err := websocket.Dial(context.Background(), "ws"+server.URL[4:]+"/api/v1/live", nil)
|
|
if err == nil {
|
|
t.Fatal("expected unauthenticated upgrade rejection")
|
|
}
|
|
if response == nil || response.StatusCode != http.StatusUnauthorized {
|
|
t.Fatalf("response=%v err=%v", response, err)
|
|
}
|
|
}
|
|
|
|
func TestHandlerRejectsCrossOriginUpgrade(t *testing.T) {
|
|
server := authorizedServer(t, live.Handler{Planner: plannerForTest(t)})
|
|
defer server.Close()
|
|
options := &websocket.DialOptions{HTTPHeader: http.Header{"Origin": []string{"https://evil.example"}}}
|
|
_, response, err := websocket.Dial(context.Background(), "ws"+server.URL[4:]+"/api/v1/live", options)
|
|
if err == nil {
|
|
t.Fatal("expected cross-origin rejection")
|
|
}
|
|
if response == nil || response.StatusCode != http.StatusForbidden {
|
|
t.Fatalf("response=%v err=%v", response, err)
|
|
}
|
|
}
|
|
|
|
func TestHandlerClosesWhenAuthenticatedRequestIsCancelled(t *testing.T) {
|
|
cancelled := make(chan context.CancelFunc, 1)
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
ctx, cancel := context.WithCancel(auth.WithPrincipal(r.Context(), auth.Principal{Subject: "viewer", Role: auth.RoleViewer}))
|
|
cancelled <- cancel
|
|
live.Handler{}.ServeHTTP(w, r.WithContext(ctx))
|
|
}))
|
|
defer server.Close()
|
|
conn, _, err := websocket.Dial(context.Background(), "ws"+server.URL[4:]+"/api/v1/live", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.CloseNow()
|
|
(<-cancelled)()
|
|
readCtx, cancelRead := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancelRead()
|
|
if _, _, err := conn.Read(readCtx); err == nil {
|
|
t.Fatal("live connection survived authenticated request cancellation")
|
|
}
|
|
}
|
|
|
|
func TestHandlerAuthorizesSubscriptionAndSequencesSamples(t *testing.T) {
|
|
server := authorizedServer(t, live.Handler{Planner: plannerForTest(t), Sampler: sampler{}})
|
|
defer server.Close()
|
|
conn, _, err := websocket.Dial(context.Background(), "ws"+server.URL[4:]+"/api/v1/live", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close(websocket.StatusNormalClosure, "")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
if err := wsjson.Write(ctx, conn, map[string]any{"schemaVersion": 1, "type": "subscribe", "subscriptionId": "sub-1", "query": validQuery(), "intervalSeconds": 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var status live.StatusMessage
|
|
if err := wsjson.Read(ctx, conn, &status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status.State != "subscribed" {
|
|
t.Fatalf("status=%+v", status)
|
|
}
|
|
var first, second live.SamplesMessage
|
|
if err := wsjson.Read(ctx, conn, &first); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := wsjson.Read(ctx, conn, &second); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if first.Sequence != 1 || second.Sequence != 2 || second.Sequence <= first.Sequence {
|
|
t.Fatalf("sequences=%d,%d", first.Sequence, second.Sequence)
|
|
}
|
|
if len(first.Samples) != 1 || first.Samples[0].Freshness != "fresh" {
|
|
t.Fatalf("samples=%+v", first.Samples)
|
|
}
|
|
}
|
|
|
|
func TestHandlerReportsMissingSamplerInsteadOfEmptySamples(t *testing.T) {
|
|
server := authorizedServer(t, live.Handler{Planner: plannerForTest(t)})
|
|
defer server.Close()
|
|
conn, _, err := websocket.Dial(context.Background(), "ws"+server.URL[4:]+"/api/v1/live", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() { _ = conn.Close(websocket.StatusNormalClosure, "") }()
|
|
ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
|
|
defer cancel()
|
|
if err := wsjson.Write(ctx, conn, map[string]any{"schemaVersion": 1, "type": "subscribe", "subscriptionId": "sub-1", "query": validQuery(), "intervalSeconds": 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var status live.StatusMessage
|
|
if err := wsjson.Read(ctx, conn, &status); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status.State != "subscribed" {
|
|
t.Fatalf("status=%+v", status)
|
|
}
|
|
var failure live.ErrorMessage
|
|
if err := wsjson.Read(ctx, conn, &failure); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failure.Type != "error" || failure.Code != "LIVE_SAMPLE_UNAVAILABLE" || failure.SubscriptionID != "sub-1" {
|
|
t.Fatalf("failure=%+v", failure)
|
|
}
|
|
}
|
|
|
|
func TestHandlerDeniesInvalidSubscriptionAndLimitsMessages(t *testing.T) {
|
|
server := authorizedServer(t, live.Handler{Planner: plannerForTest(t), MaxMessages: 2, RateWindow: time.Minute})
|
|
defer server.Close()
|
|
conn, _, err := websocket.Dial(context.Background(), "ws"+server.URL[4:]+"/api/v1/live", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close(websocket.StatusNormalClosure, "")
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
if err := wsjson.Write(ctx, conn, map[string]any{"schemaVersion": 1, "type": "subscribe", "subscriptionId": "bad", "query": map[string]any{"metric": "unknown"}, "intervalSeconds": 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var problem live.ErrorMessage
|
|
if err := wsjson.Read(ctx, conn, &problem); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if problem.Code != "LIVE_SUBSCRIPTION_DENIED" {
|
|
t.Fatalf("problem=%+v", problem)
|
|
}
|
|
if err := wsjson.Write(ctx, conn, map[string]any{"schemaVersion": 1, "type": "ping", "nonce": "one"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var pong live.PongMessage
|
|
if err := wsjson.Read(ctx, conn, &pong); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := wsjson.Write(ctx, conn, map[string]any{"schemaVersion": 1, "type": "ping", "nonce": "two"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var limited live.ErrorMessage
|
|
if err := wsjson.Read(ctx, conn, &limited); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if limited.Code != "LIVE_RATE_LIMIT" {
|
|
t.Fatalf("problem=%+v", limited)
|
|
}
|
|
}
|
|
|
|
func TestHandlerClosesOversizedMessage(t *testing.T) {
|
|
server := authorizedServer(t, live.Handler{Planner: plannerForTest(t)})
|
|
defer server.Close()
|
|
conn, _, err := websocket.Dial(context.Background(), "ws"+server.URL[4:]+"/api/v1/live", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close(websocket.StatusNormalClosure, "")
|
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
defer cancel()
|
|
if err := wsjson.Write(ctx, conn, map[string]any{"schemaVersion": 1, "type": "ping", "nonce": strings.Repeat("x", 70000)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var response map[string]any
|
|
if err := wsjson.Read(ctx, conn, &response); err == nil {
|
|
t.Fatal("expected oversized message connection to close")
|
|
}
|
|
}
|