File size: 4,515 Bytes
1f10f31
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
package oasmiddleware_test

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/getkin/kin-openapi/routers"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"

	api "github.com/openmeterio/openmeter/api/v3"
	"github.com/openmeterio/openmeter/api/v3/oasmiddleware"
)

// TestValidateResponse_Violation proves that the ValidateResponse middleware fires its
// error hook when a handler returns a response body that violates the OpenAPI spec.
//
// GET /openmeter/addons requires a 200 body with both "data" and "meta" fields.
// Returning {} omits both required fields and must trigger a violation.
func TestValidateResponse_Violation(t *testing.T) {
	swagger, err := api.GetSwagger()
	require.NoError(t, err)

	swagger.Servers = nil

	router, err := oasmiddleware.NewValidationRouter(t.Context(), swagger, &oasmiddleware.ValidationRouterOpts{
		DeleteServers: true,
	})
	require.NoError(t, err)

	var gotErr error
	mw := oasmiddleware.ValidateResponse(router, oasmiddleware.ValidateResponseOption{
		ResponseValidationErrorHook: func(err error, r *http.Request) {
			gotErr = err
		},
	})

	badHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{}`))
	})

	req := httptest.NewRequest(http.MethodGet, "/openmeter/addons", nil)
	rec := httptest.NewRecorder()

	mw(badHandler).ServeHTTP(rec, req)

	assert.Error(t, gotErr, "expected a validation error for missing required fields")
	t.Logf("validation error (expected): %v", gotErr)
}

// TestValidateResponse_Clean proves that a well-formed response does not trigger the error hook.
func TestValidateResponse_Clean(t *testing.T) {
	swagger, err := api.GetSwagger()
	require.NoError(t, err)

	swagger.Servers = nil

	router, err := oasmiddleware.NewValidationRouter(t.Context(), swagger, &oasmiddleware.ValidationRouterOpts{
		DeleteServers: true,
	})
	require.NoError(t, err)

	var gotErr error
	mw := oasmiddleware.ValidateResponse(router, oasmiddleware.ValidateResponseOption{
		ResponseValidationErrorHook: func(err error, r *http.Request) {
			gotErr = err
		},
	})

	goodHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{"data":[],"meta":{"page":{"number":0,"size":100,"total":0}}}`))
	})

	req := httptest.NewRequest(http.MethodGet, "/openmeter/addons", nil)
	rec := httptest.NewRecorder()

	mw(goodHandler).ServeHTTP(rec, req)

	assert.NoError(t, gotErr, "expected no validation error for a well-formed response")
}

// TestValidateResponse_RouteFilterSkipsValidation proves that when RouteFilterHook returns false,
// the response body is neither buffered nor validated — even if it would otherwise violate the spec.
// The filter is the per-route gate that lets callers (e.g. unstable-only mode) avoid the
// buffering overhead on routes they don't care about.
func TestValidateResponse_RouteFilterSkipsValidation(t *testing.T) {
	swagger, err := api.GetSwagger()
	require.NoError(t, err)

	swagger.Servers = nil

	router, err := oasmiddleware.NewValidationRouter(t.Context(), swagger, &oasmiddleware.ValidationRouterOpts{
		DeleteServers: true,
	})
	require.NoError(t, err)

	var (
		gotErr        error
		filteredRoute *routers.Route
	)
	mw := oasmiddleware.ValidateResponse(router, oasmiddleware.ValidateResponseOption{
		RouteFilterHook: func(route *routers.Route) bool {
			filteredRoute = route
			return false
		},
		ResponseValidationErrorHook: func(err error, r *http.Request) {
			gotErr = err
		},
	})

	// Body that would fail validation if the filter let it through.
	badHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		w.WriteHeader(http.StatusOK)
		_, _ = w.Write([]byte(`{}`))
	})

	req := httptest.NewRequest(http.MethodGet, "/openmeter/addons", nil)
	rec := httptest.NewRecorder()

	mw(badHandler).ServeHTTP(rec, req)

	require.NotNil(t, filteredRoute, "RouteFilterHook should have been invoked with the matched route")
	assert.Equal(t, "/openmeter/addons", filteredRoute.Path)
	assert.NoError(t, gotErr, "validation error hook must not fire when the filter returns false")
	assert.Equal(t, http.StatusOK, rec.Code, "client response should still be served")
	assert.Equal(t, `{}`, rec.Body.String(), "client response body should still be served")
}