package infra import ( "os" "testing" ) // findNatsSample devuelve el primer PromSample cuyo Name coincide y cuyos labels // extra (clave/valor alternados) están todos presentes con el valor esperado. // El segundo retorno indica si se encontró. func findNatsSample(samples []PromSample, name string, labels ...string) (PromSample, bool) { for _, s := range samples { if s.Name != name { continue } match := true for i := 0; i+1 < len(labels); i += 2 { if s.Labels[labels[i]] != labels[i+1] { match = false break } } if match { return s, true } } return PromSample{}, false } func mustRead(t *testing.T, path string) []byte { t.Helper() b, err := os.ReadFile(path) if err != nil { t.Fatalf("read fixture %s: %v", path, err) } return b } // golden: fixtures reales de un nats-server 2.11.15, node="probe" (== el leader // de los streams), valores concretos verificados a mano. func TestParseNatsMonitorGolden(t *testing.T) { varz := mustRead(t, "testdata/nats_varz.json") connz := mustRead(t, "testdata/nats_connz.json") jsz := mustRead(t, "testdata/nats_jsz.json") got, err := ParseNatsMonitor("probe", varz, connz, jsz) if err != nil { t.Fatalf("unexpected error: %v", err) } want := map[string]float64{ "nats_msgs_in_total": 17, "nats_msgs_out_total": 17, "nats_mem_bytes": 18288640, "nats_jetstream_streams": 3, "nats_connections": 1, "nats_jetstream_messages": 6, } for name, w := range want { s, ok := findNatsSample(got, name) if !ok { t.Errorf("missing sample %q", name) continue } if s.Value != w { t.Errorf("%s = %v, want %v", name, s.Value, w) } if s.Labels["node"] != "probe" || s.Labels["instance"] != "probe" { t.Errorf("%s labels = %v, want node=instance=probe", name, s.Labels) } } // kv_bucket_msgs por cada KV bucket (prefijo KV_ recortado). for bucket, w := range map[string]float64{ "UNIBUS_users": 2, "UNIBUS_rooms": 2, "UNIBUS_members": 2, } { s, ok := findNatsSample(got, "kv_bucket_msgs", "bucket", bucket) if !ok { t.Errorf("missing kv_bucket_msgs{bucket=%q}", bucket) continue } if s.Value != w { t.Errorf("kv_bucket_msgs{bucket=%q} = %v, want %v", bucket, s.Value, w) } } // raft leader: probe == node, así que el stream KV_UNIBUS_users tiene leader=1. s, ok := findNatsSample(got, "nats_jetstream_raft_leader", "stream", "KV_UNIBUS_users") if !ok { t.Fatal("missing nats_jetstream_raft_leader{stream=KV_UNIBUS_users}") } if s.Value != 1 { t.Errorf("nats_jetstream_raft_leader{stream=KV_UNIBUS_users} = %v, want 1", s.Value) } // stream_detail también emite nats_stream_messages con label stream completo. if s, ok := findNatsSample(got, "nats_stream_messages", "stream", "KV_UNIBUS_users"); !ok || s.Value != 2 { t.Errorf("nats_stream_messages{stream=KV_UNIBUS_users} = %v ok=%v, want 2", s.Value, ok) } // nats_server_start_seconds presente (start es RFC3339 válido). if _, ok := findNatsSample(got, "nats_server_start_seconds"); !ok { t.Error("missing nats_server_start_seconds (start is a valid RFC3339)") } } // edge: jsz sin streams ni account_details. No produce series kv_bucket_msgs ni // nats_stream_*, pero sí las de varz/connz y las jetstream top-level (en 0). func TestParseNatsMonitorEmptyJsz(t *testing.T) { varz := mustRead(t, "testdata/nats_varz.json") connz := mustRead(t, "testdata/nats_connz.json") jsz := []byte(`{"streams":0,"account_details":[]}`) got, err := ParseNatsMonitor("probe", varz, connz, jsz) if err != nil { t.Fatalf("unexpected error: %v", err) } if _, ok := findNatsSample(got, "kv_bucket_msgs", "bucket", "UNIBUS_users"); ok { t.Error("did not expect kv_bucket_msgs with empty account_details") } if _, ok := findNatsSample(got, "nats_stream_messages"); ok { t.Error("did not expect nats_stream_messages with empty account_details") } // varz/connz siguen presentes. if s, ok := findNatsSample(got, "nats_msgs_in_total"); !ok || s.Value != 17 { t.Errorf("nats_msgs_in_total = %v ok=%v, want 17", s.Value, ok) } if s, ok := findNatsSample(got, "nats_connections"); !ok || s.Value != 1 { t.Errorf("nats_connections = %v ok=%v, want 1", s.Value, ok) } } // edge: connz inválido. No es error; nats_connections cae a varz.connections (1). // varz/jsz siguen produciendo sus series. func TestParseNatsMonitorInvalidConnz(t *testing.T) { varz := mustRead(t, "testdata/nats_varz.json") jsz := mustRead(t, "testdata/nats_jsz.json") got, err := ParseNatsMonitor("probe", varz, []byte("not json"), jsz) if err != nil { t.Fatalf("unexpected error: %v", err) } // fallback a varz.connections (= 1). if s, ok := findNatsSample(got, "nats_connections"); !ok || s.Value != 1 { t.Errorf("nats_connections = %v ok=%v, want 1 (fallback varz.connections)", s.Value, ok) } // jsz sigue vivo. if s, ok := findNatsSample(got, "nats_jetstream_streams"); !ok || s.Value != 3 { t.Errorf("nats_jetstream_streams = %v ok=%v, want 3", s.Value, ok) } } // error path: varz inválido devuelve error no-nil (es el core, sin él no hay nada). func TestParseNatsMonitorInvalidVarz(t *testing.T) { if _, err := ParseNatsMonitor("probe", []byte("{{{"), nil, nil); err == nil { t.Fatal("expected error for invalid varz, got nil") } }