Ctrl K
ComunidadGuías…

pkg/runtimeevidence/evidence_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package runtimeevidence

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"strings"
	"testing"
)

const alarms = "# TYPE rabbitmq_alarms_free_disk_space_watermark gauge\nrabbitmq_alarms_free_disk_space_watermark 0\n# TYPE rabbitmq_alarms_memory_used_watermark gauge\nrabbitmq_alarms_memory_used_watermark 0\n"

func TestRabbitMQAlarmAndRecovery(t *testing.T) {
	for _, active := range []bool{false, true, false} {
		body := alarms
		if active {
			body = strings.Replace(body, "watermark 0", "watermark 1", 1)
		}
		facts, err := Parse(RabbitMQ, 200, []byte(body))
		if err != nil {
			t.Fatal(err)
		}
		if facts.RabbitMQ == nil || facts.RabbitMQ.DiskAlarm != active || facts.RabbitMQ.MemoryAlarm {
			t.Fatalf("unexpected facts: %+v", facts.RabbitMQ)
		}
	}
}

func TestRabbitMQUnavailableNeverMeansNoAlarms(t *testing.T) {
	tests := []struct {
		name, body string
		reason     Reason
	}{
		{"empty", "", MissingAlarmMetrics},
		{"one alarm absent", "rabbitmq_alarms_free_disk_space_watermark 0\n", MissingAlarmMetrics},
		{"duplicate equal", alarms + "rabbitmq_alarms_free_disk_space_watermark 0\n", AmbiguousAlarmMetric},
		{"multiple labeled nodes", alarms + "rabbitmq_alarms_free_disk_space_watermark{node=\"other\"} 1\n", AmbiguousAlarmMetric},
		{"invalid value", strings.Replace(alarms, "watermark 0", "watermark 2", 1), InvalidAlarmMetric},
		{"nan", strings.Replace(alarms, "watermark 0", "watermark NaN", 1), InvalidAlarmMetric},
		{"infinity", strings.Replace(alarms, "watermark 0", "watermark +Inf", 1), InvalidAlarmMetric},
		{"counter instead of gauge", strings.Replace(alarms, " gauge", " counter", 1), InvalidAlarmMetric},
		{"invalid label", strings.Replace(alarms, "watermark 0", "watermark{node=unquoted} 0", 1), InvalidAlarmMetric},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			facts, err := Parse(RabbitMQ, 200, []byte(test.body))
			if !errors.Is(err, test.reason) {
				t.Fatalf("got %v, want %v", err, test.reason)
			}
			if facts != (Facts{}) {
				t.Fatalf("failed parser leaked facts: %+v", facts)
			}
		})
	}
}

func TestNATSBacklogRecoveryRetainsMessages(t *testing.T) {
	for _, pending := range []int{5, 0} {
		facts, err := Parse(NATS, 200, []byte(natsBody(1, 1, 1, 1, pending)))
		if err != nil {
			t.Fatal(err)
		}
		n := facts.NATS
		if n.Coverage != "node_reported" || !n.JetStreamEnabled || n.Totals.Messages != 5 || n.Consumers[0].Pending != uint64(pending) || n.Consumers[0].Account != "A0" {
			t.Fatalf("unexpected facts: %+v", n)
		}
	}
}

func TestNATSCoverage(t *testing.T) {
	tests := []struct {
		name, body, coverage string
		rows                 int
		truncated            bool
	}{
		{"disabled", `{"disabled":true}`, "disabled", 0, false},
		{"enabled empty", `{"total":0,"streams":0,"consumers":0,"messages":0}`, "node_reported", 0, false},
		{"missing consumer details", `{"total":1,"streams":1,"consumers":1,"messages":5}`, "partial", 0, false},
		{"missing empty accounts", `{"total":2,"streams":0,"consumers":0,"messages":0}`, "partial", 0, false},
		{"missing streams without consumers", `{"total":0,"streams":2,"consumers":0,"messages":5}`, "partial", 0, false},
		{"row cap", natsBody(1, 1, 21, 21, 5), "partial", 20, true},
		{"changing source counts", natsBody(1, 1, 0, 1, 5), "partial", 1, false},
	}
	for _, test := range tests {
		t.Run(test.name, func(t *testing.T) {
			facts, err := Parse(NATS, 200, []byte(test.body))
			if err != nil {
				t.Fatal(err)
			}
			n := facts.NATS
			if n.Coverage != test.coverage || len(n.Consumers) != test.rows || n.Truncated != test.truncated {
				t.Fatalf("unexpected facts: %+v", n)
			}
			if n.Coverage == "disabled" && (n.Totals != nil || n.JetStreamEnabled) {
				t.Fatal("disabled should not fabricate totals")
			}
		})
	}
}

func TestNATSAccountIdentity(t *testing.T) {
	var response map[string]any
	if err := json.Unmarshal([]byte(natsBody(2, 2, 2, 1, 5)), &response); err != nil {
		t.Fatal(err)
	}
	accounts := response["account_details"].([]any)
	var second map[string]any
	if err := json.Unmarshal([]byte(`{"id":"A1","name":"same display name","stream_detail":[{"name":"LAB","consumer_detail":[{"name":"idle0","num_pending":2,"num_ack_pending":0,"num_redelivered":0}]}]}`), &second); err != nil {
		t.Fatal(err)
	}
	response["account_details"] = append(accounts, second)
	body, _ := json.Marshal(response)
	facts, err := Parse(NATS, 200, body)
	if err != nil {
		t.Fatal(err)
	}
	if facts.NATS.Coverage != "node_reported" || len(facts.NATS.Consumers) != 2 || facts.NATS.Consumers[0].Account == facts.NATS.Consumers[1].Account {
		t.Fatalf("account identity lost: %+v", facts.NATS)
	}
}

func TestNATSRejectsMissingInvalidAndAmbiguousFacts(t *testing.T) {
	valid := natsBody(1, 1, 1, 1, 5)
	tests := []string{
		`{}`, `null`, `[]`, `{"disabled":"true"}`, `{"total":0,"streams":0,"consumers":0}`,
		strings.Replace(valid, `"num_pending":5`, `"other":5`, 1),
		strings.Replace(valid, `"num_pending":5`, `"num_pending":null`, 1),
		strings.Replace(valid, `"num_pending":5`, `"num_pending":-1`, 1),
		strings.Replace(valid, `"num_pending":5`, `"num_pending":1.5`, 1),
		strings.Replace(valid, `"num_pending":5`, `"num_pending":true`, 1),
		strings.Replace(valid, `"id":"A0"`, `"id":""`, 1),
		strings.Replace(valid, `"id":"A0"`, `"id":"A\n0"`, 1),
		strings.Replace(valid, `"id":"A0"`, `"id":"`+strings.Repeat("a", 257)+`"`, 1),
		strings.Replace(natsBody(1, 1, 2, 2, 5), `"idle1"`, `"idle0"`, 1),
	}
	for i, body := range tests {
		t.Run(fmt.Sprint(i), func(t *testing.T) {
			facts, err := Parse(NATS, 200, []byte(body))
			if !errors.Is(err, UnexpectedShape) || facts != (Facts{}) {
				t.Fatalf("got %+v, %v", facts, err)
			}
		})
	}
}

func TestVaultReportsFactsIndependentOfStatus(t *testing.T) {
	tests := []struct {
		status                       int
		body                         string
		initialized, sealed, standby bool
	}{
		{501, `{"initialized":false,"sealed":true,"standby":true}`, false, true, true},
		{503, `{"initialized":true,"sealed":true,"standby":true}`, true, true, true},
		{200, `{"initialized":true,"sealed":false,"standby":false}`, true, false, false},
		{429, `{"initialized":true,"sealed":false,"standby":true}`, true, false, true},
		{200, `{"initialized":true,"sealed":true,"standby":true}`, true, true, true},
		{499, `{"initialized":true,"sealed":false,"standby":true}`, true, false, true},
	}
	for _, test := range tests {
		facts, err := Parse(Vault, test.status, []byte(test.body))
		if err != nil {
			t.Fatal(err)
		}
		v := facts.Vault
		if v.Initialized != test.initialized || v.Sealed != test.sealed || v.Standby != test.standby {
			t.Fatalf("unexpected facts: %+v", v)
		}
	}
}

func TestVaultMissingOrMalformedDoesNotBecomeFalse(t *testing.T) {
	for _, body := range []string{`{}`, `null`, `{"initialized":false,"sealed":false}`, `{"initialized":true,"sealed":null,"standby":false}`, `{"initialized":true,"sealed":"false","standby":false}`, `{"initialized":true,"sealed":false,"standby":false} {}`} {
		facts, err := Parse(Vault, 200, []byte(body))
		if !errors.Is(err, UnexpectedShape) || facts != (Facts{}) {
			t.Fatalf("got %+v, %v", facts, err)
		}
	}
}

func TestFactsNeverIncludeUnselectedFields(t *testing.T) {
	tests := []struct {
		adapter Adapter
		body    string
	}{
		{RabbitMQ, alarms + "irrelevant_metric{password=\"secret-sentinel\"} 1\n"},
		{NATS, strings.Replace(natsBody(1, 1, 1, 1, 5), `"id":"A0"`, `"id":"A0","config":{"password":"secret-sentinel"}`, 1)},
		{Vault, `{"initialized":true,"sealed":false,"standby":false,"token":"secret-sentinel","cluster_name":"secret-sentinel","performance_standby":false}`},
	}
	for _, test := range tests {
		facts, err := Parse(test.adapter, 200, []byte(test.body))
		if err != nil {
			t.Fatal(err)
		}
		output, _ := json.Marshal(facts)
		if strings.Contains(string(output), "secret-sentinel") {
			t.Fatalf("leaked sensitive field: %s", output)
		}
	}
}

func TestEndpointFailuresNeverReturnFacts(t *testing.T) {
	for _, adapter := range []Adapter{RabbitMQ, NATS, Vault} {
		for _, status := range []int{401, 403, 301, 302, 307, 308} {
			facts, err := Parse(adapter, status, []byte(`{"initialized":true,"sealed":false,"standby":false}`))
			if err == nil || facts != (Facts{}) {
				t.Fatalf("%s status %d: %+v %v", adapter, status, facts, err)
			}
		}
	}
	for _, adapter := range []Adapter{RabbitMQ, NATS} {
		if _, err := Parse(adapter, 503, []byte(alarms)); !errors.Is(err, UnexpectedHTTPStatus) {
			t.Fatal(err)
		}
	}
	if _, err := Parse(Vault, 200, []byte(strings.Repeat("x", MaxBodyBytes+1))); !errors.Is(err, ResponseTooLarge) {
		t.Fatal(err)
	}
	if _, err := Parse("other", 200, nil); !errors.Is(err, UnsupportedAdapter) {
		t.Fatal(err)
	}
}

func natsBody(accounts, streams, consumers, rows, pending int) string {
	var details []string
	for i := range rows {
		details = append(details, fmt.Sprintf(`{"name":"idle%d","num_pending":%d,"num_ack_pending":0,"num_redelivered":0}`, i, pending))
	}
	return fmt.Sprintf(`{"total":%d,"streams":%d,"consumers":%d,"messages":5,"account_details":[{"id":"A0","name":"same display name","stream_detail":[{"name":"LAB","consumer_detail":[%s]}]}]}`, accounts, streams, consumers, strings.Join(details, ","))
}

func TestConflictingJSONDoesNotChooseLastValue(t *testing.T) {
	for _, body := range []string{
		`{"initialized":true,"sealed":true,"sealed":false,"standby":false}`,
		`{"initialized":true,"sealed":true,"Sealed":false,"standby":false}`,
		`{"initialized":true,"sealed":true,"s\u0065aled":false,"standby":false}`,
		`{"initialized":true,"sealed":true,"ſealed":false,"standby":false}`,
		`{"initialized":true,"sealed":true,"ſtandby":true,"standby":false}`,
		`{"initialized":true,"sealed":true,"standby":false,"removed_from_cluster":false,"removed_from_cluſter":true}`,
		`{"initialized":true,"sealed":true,"standby":false,"key":true,"Key":false}`,
		`{"initialized":true,"sealed":true,"standby":false,"Σ":true,"ς":false}`,
	} {
		facts, err := Parse(Vault, 200, []byte(body))
		if !errors.Is(err, UnexpectedShape) || facts != (Facts{}) {
			t.Fatalf("got %+v %v", facts, err)
		}
	}
}

func TestMalformedJSONIsBoundedAndSanitized(t *testing.T) {
	for _, body := range [][]byte{
		[]byte(`{"initialized":true,"sealed":false,"standby":false,"extra":` + strings.Repeat("[", 66) + strings.Repeat("]", 66) + `}`),
		append([]byte(`{"token":"secret-sentinel`), 0xff),
		[]byte(`{"token":"secret-sentinel"`),
	} {
		facts, err := Parse(Vault, 200, body)
		if !errors.Is(err, UnexpectedShape) || facts != (Facts{}) {
			t.Fatalf("got %+v %v", facts, err)
		}
	}
}

func TestCapturedNATSEndpoints(t *testing.T) {
	for _, scenario := range []struct {
		name    string
		pending uint64
	}{{"backlog", 5}, {"recovered", 0}} {
		body, err := os.ReadFile("testdata/nats-" + scenario.name + ".json")
		if err != nil {
			t.Fatal(err)
		}
		facts, err := Parse(NATS, 200, body)
		if err != nil {
			t.Fatal(err)
		}
		n := facts.NATS
		if n.Coverage != "node_reported" || n.Totals.Messages != 5 || len(n.Consumers) != 1 || n.Consumers[0].Account != "$G" || n.Consumers[0].Pending != scenario.pending {
			t.Fatalf("unexpected facts: %+v", n)
		}
	}
}

func TestJSONCaseFoldingPreservesDistinctKeys(t *testing.T) {
	var decoded map[string]any
	if err := decode([]byte(`{"sealed":true,"standby":false,"status":"ready","σ":1,"τ":2}`), &decoded); err != nil {
		t.Fatalf("distinct keys rejected: %v", err)
	}
	if len(decoded) != 5 || decoded["sealed"] != true || decoded["standby"] != false {
		t.Fatalf("distinct observations changed: %+v", decoded)
	}
}