prometheus/web/api/v1/openapi_test.go
Arve Knudsen 51d33be878
web: switch from gopkg.in/yaml to go.yaml.in/yaml (#17979)
Replace gopkg.in/yaml.v2 and gopkg.in/yaml.v3 imports with
go.yaml.in/yaml/v2 and go.yaml.in/yaml/v3 respectively.

Signed-off-by: Arve Knudsen <arve.knudsen@gmail.com>
2026-02-03 14:15:35 +00:00

289 lines
8.7 KiB
Go

// Copyright The Prometheus Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/prometheus/common/promslog"
"github.com/stretchr/testify/require"
"go.yaml.in/yaml/v2"
)
// TestOpenAPIHTTPHandler verifies that the OpenAPI endpoint serves a valid specification
// with correct headers, structure conforming to OpenAPI 3.1 standards, and consistent responses.
func TestOpenAPIHTTPHandler(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
// First request.
req1 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec1 := httptest.NewRecorder()
builder.ServeOpenAPI(rec1, req1)
// Verify status code and headers.
require.Equal(t, http.StatusOK, rec1.Code)
require.True(t, strings.HasPrefix(rec1.Header().Get("Content-Type"), "application/yaml"), "Content-Type should start with application/yaml")
require.Equal(t, "no-cache, no-store, must-revalidate", rec1.Header().Get("Cache-Control"))
// Verify it is valid YAML.
var spec map[string]any
err := yaml.Unmarshal(rec1.Body.Bytes(), &spec)
require.NoError(t, err)
// Verify structure.
require.Contains(t, spec, "openapi")
require.Contains(t, spec, "info")
require.Contains(t, spec, "paths")
require.Contains(t, spec, "components")
// Verify OpenAPI version (default is 3.1.0).
require.Equal(t, "3.1.0", spec["openapi"])
// Verify info section.
info, ok := spec["info"].(map[any]any)
require.True(t, ok, "info should be a map")
require.Equal(t, "Prometheus API", info["title"])
// Verify paths exist.
paths, ok := spec["paths"].(map[any]any)
require.True(t, ok, "paths should be a map")
require.NotEmpty(t, paths, "paths should not be empty")
// Second request to verify response consistency.
req2 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec2 := httptest.NewRecorder()
builder.ServeOpenAPI(rec2, req2)
// Both responses should be identical.
require.Equal(t, rec1.Body.String(), rec2.Body.String())
}
// TestOpenAPIPathFiltering verifies that the IncludePaths option correctly filters
// which API paths are included in the generated specification.
func TestOpenAPIPathFiltering(t *testing.T) {
tests := []struct {
name string
includePaths []string
wantPaths []string
excludePaths []string
}{
{
name: "no filter includes all",
includePaths: nil,
wantPaths: []string{"/query", "/labels", "/alerts", "/targets"},
},
{
name: "filter query paths",
includePaths: []string{"/query"},
wantPaths: []string{"/query", "/query_range", "/query_exemplars"},
excludePaths: []string{"/labels", "/alerts", "/targets"},
},
{
name: "filter status paths",
includePaths: []string{"/status"},
wantPaths: []string{"/status/config", "/status/flags", "/status/runtimeinfo"},
excludePaths: []string{"/query", "/alerts", "/targets"},
},
{
name: "filter multiple prefixes",
includePaths: []string{"/label", "/series"},
wantPaths: []string{"/labels", "/label/{name}/values", "/series"},
excludePaths: []string{"/query", "/alerts", "/targets"},
},
{
name: "exact path match",
includePaths: []string{"/alerts"},
wantPaths: []string{"/alerts"},
excludePaths: []string{"/alertmanagers", "/query"},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{
IncludePaths: tc.includePaths,
}, promslog.NewNopLogger())
req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec := httptest.NewRecorder()
builder.ServeOpenAPI(rec, req)
require.Equal(t, http.StatusOK, rec.Code)
var spec map[string]any
err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
require.NoError(t, err)
paths, ok := spec["paths"].(map[any]any)
require.True(t, ok, "paths should be a map")
for _, want := range tc.wantPaths {
require.Contains(t, paths, want)
}
for _, exclude := range tc.excludePaths {
require.NotContains(t, paths, exclude)
}
})
}
}
// TestOpenAPISchemaCompleteness verifies that all referenced schemas in paths
// are defined in the components/schemas section of the specification.
func TestOpenAPISchemaCompleteness(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
req := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec := httptest.NewRecorder()
builder.ServeOpenAPI(rec, req)
var spec map[string]any
err := yaml.Unmarshal(rec.Body.Bytes(), &spec)
require.NoError(t, err)
components, ok := spec["components"].(map[any]any)
require.True(t, ok, "components should be a map")
schemas, ok := components["schemas"].(map[any]any)
require.True(t, ok, "schemas should be a map")
// Verify essential schemas are present.
essentialSchemas := []string{
"Error",
"Labels",
"QueryOutputBody",
"LabelsOutputBody",
"SeriesOutputBody",
"TargetsOutputBody",
"AlertsOutputBody",
"RulesOutputBody",
"StatusConfigOutputBody",
"StatusFlagsOutputBody",
"PrometheusVersion",
}
for _, schema := range essentialSchemas {
require.Contains(t, schemas, schema)
}
}
// TODO: Add test to verify all routes from api.go Register() are covered in OpenAPI spec.
// Consider wrapping Router to track registered paths and cross-check with OpenAPI paths.
// TestOpenAPIShouldIncludePath verifies the shouldIncludePath method correctly
// matches paths against the IncludePaths filter configuration.
func TestOpenAPIShouldIncludePath(t *testing.T) {
tests := []struct {
name string
includePaths []string
path string
expected bool
}{
{
name: "empty filter includes all",
includePaths: nil,
path: "/query",
expected: true,
},
{
name: "exact match",
includePaths: []string{"/query"},
path: "/query",
expected: true,
},
{
name: "prefix match",
includePaths: []string{"/query"},
path: "/query_range",
expected: true,
},
{
name: "no match",
includePaths: []string{"/query"},
path: "/labels",
expected: false,
},
{
name: "multiple filters with match",
includePaths: []string{"/labels", "/series"},
path: "/series",
expected: true,
},
{
name: "multiple filters without match",
includePaths: []string{"/labels", "/series"},
path: "/query",
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
builder := &OpenAPIBuilder{
options: OpenAPIOptions{
IncludePaths: tc.includePaths,
},
}
result := builder.shouldIncludePath(tc.path)
require.Equal(t, tc.expected, result)
})
}
}
// TestOpenAPIVersionConsistency verifies that both OpenAPI versions are properly generated
// and that 3.2 has exactly one more path than 3.1 (/notifications/live).
func TestOpenAPIVersionConsistency(t *testing.T) {
builder := NewOpenAPIBuilder(OpenAPIOptions{}, promslog.NewNopLogger())
// Fetch OpenAPI 3.1 spec (default).
req31 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml", nil)
rec31 := httptest.NewRecorder()
builder.ServeOpenAPI(rec31, req31)
require.Equal(t, http.StatusOK, rec31.Code)
// Fetch OpenAPI 3.2 spec.
req32 := httptest.NewRequest(http.MethodGet, "/api/v1/openapi.yaml?openapi_version=3.2", nil)
rec32 := httptest.NewRecorder()
builder.ServeOpenAPI(rec32, req32)
require.Equal(t, http.StatusOK, rec32.Code)
// Parse both specs.
var spec31, spec32 map[string]any
err := yaml.Unmarshal(rec31.Body.Bytes(), &spec31)
require.NoError(t, err)
err = yaml.Unmarshal(rec32.Body.Bytes(), &spec32)
require.NoError(t, err)
// Verify versions are different.
require.Equal(t, "3.1.0", spec31["openapi"])
require.Equal(t, "3.2.0", spec32["openapi"])
// Verify /notifications/live is only in 3.2.
paths31 := spec31["paths"].(map[any]any)
paths32 := spec32["paths"].(map[any]any)
require.NotContains(t, paths31, "/notifications/live")
require.Contains(t, paths32, "/notifications/live")
// Verify 3.2 has exactly one more path than 3.1.
require.Len(t, paths32, len(paths31)+1,
"OpenAPI 3.2 should have exactly one more path than 3.1")
}