package infra import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" ) // lokiPushBody refleja la estructura JSON que espera el push API de Loki. type lokiPushBody struct { Streams []struct { Stream map[string]string `json:"stream"` Values [][2]string `json:"values"` } `json:"streams"` } func TestPushLokiStream(t *testing.T) { t.Run("JSON enviado tiene estructura streams/stream/values correcta", func(t *testing.T) { var captured lokiPushBody var contentType string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { contentType = r.Header.Get("Content-Type") raw, _ := io.ReadAll(r.Body) if err := json.Unmarshal(raw, &captured); err != nil { t.Errorf("body no es JSON valido: %v", err) } w.WriteHeader(http.StatusNoContent) })) defer srv.Close() labels := map[string]string{"instance": "lucas", "job": "journald", "unit": "ssh.service"} ts := []int64{1700000000000000001, 1700000000000000002} lines := []string{"line one", "line two"} err := PushLokiStream(srv.URL, "", "", labels, ts, lines) if err != nil { t.Fatalf("error inesperado: %v", err) } if contentType != "application/json" { t.Errorf("Content-Type = %q, want application/json", contentType) } if len(captured.Streams) != 1 { t.Fatalf("streams len = %d, want 1", len(captured.Streams)) } s := captured.Streams[0] if s.Stream["unit"] != "ssh.service" || s.Stream["job"] != "journald" || s.Stream["instance"] != "lucas" { t.Errorf("stream labels = %v, want %v", s.Stream, labels) } if len(s.Values) != 2 { t.Fatalf("values len = %d, want 2", len(s.Values)) } if s.Values[0][0] != "1700000000000000001" || s.Values[0][1] != "line one" { t.Errorf("values[0] = %v, want [1700000000000000001 line one]", s.Values[0]) } if s.Values[1][0] != "1700000000000000002" || s.Values[1][1] != "line two" { t.Errorf("values[1] = %v, want [1700000000000000002 line two]", s.Values[1]) } }) t.Run("longitudes desiguales dan error antes del POST", func(t *testing.T) { hit := false srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hit = true w.WriteHeader(http.StatusNoContent) })) defer srv.Close() ts := []int64{1, 2, 3} lines := []string{"only one"} err := PushLokiStream(srv.URL, "", "", map[string]string{"job": "x"}, ts, lines) if err == nil { t.Fatalf("se esperaba error por longitudes desiguales") } if hit { t.Errorf("no debe haber peticion HTTP cuando las longitudes no coinciden") } }) t.Run("len lines cero es no-op sin peticion", func(t *testing.T) { hit := false srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hit = true w.WriteHeader(http.StatusNoContent) })) defer srv.Close() err := PushLokiStream(srv.URL, "", "", map[string]string{"job": "x"}, []int64{}, []string{}) if err != nil { t.Fatalf("no-op no debe retornar error: %v", err) } if hit { t.Errorf("no-op no debe hacer ninguna peticion HTTP") } }) t.Run("Basic Auth presente cuando user no vacio", func(t *testing.T) { var gotUser, gotPass string var hadAuth bool srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotUser, gotPass, hadAuth = r.BasicAuth() w.WriteHeader(http.StatusNoContent) })) defer srv.Close() err := PushLokiStream(srv.URL, "tenant", "secret", map[string]string{"job": "x"}, []int64{1}, []string{"hi"}) if err != nil { t.Fatalf("error inesperado: %v", err) } if !hadAuth { t.Fatalf("se esperaba header Authorization Basic") } if gotUser != "tenant" || gotPass != "secret" { t.Errorf("basic auth = %q/%q, want tenant/secret", gotUser, gotPass) } }) t.Run("status 500 produce error", func(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte("loki rejected the push")) })) defer srv.Close() err := PushLokiStream(srv.URL, "", "", map[string]string{"job": "x"}, []int64{1}, []string{"hi"}) if err == nil { t.Fatalf("se esperaba error con status 500") } if !strings.Contains(err.Error(), "500") { t.Errorf("error no menciona el codigo 500: %v", err) } if !strings.Contains(err.Error(), "loki rejected the push") { t.Errorf("error no incluye el cuerpo de respuesta: %v", err) } }) }