Skip to content

Commit 6f46779

Browse files
EPMRPP-108935 || MCP Server. Tools. Add a user's RP project name to the HTTP header
1 parent c7335a4 commit 6f46779

File tree

9 files changed

+651
-15
lines changed

9 files changed

+651
-15
lines changed

Taskfile.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ tasks:
2929
desc: "Runs GolangCI linter"
3030
cmd: "{{.GOLANG_CI}} run ./..."
3131

32+
checks:
33+
desc: "Runs checks (formatter and linter)"
34+
cmd: |
35+
go mod tidy && {{.GOLANG_CI}} fmt ./... && {{.GOLANG_CI}} run ./...
36+
3237
test:
3338
desc: "Runs tests"
3439
cmd: "go test ./..."

internal/reportportal/http_token_middleware.go

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111
const (
1212
// RPTokenContextKey is used to store RP API token in request context
1313
RPTokenContextKey contextKey = "rp_api_token" //nolint:gosec // This is a context key, not a credential
14+
// RPProjectContextKey is used to store RP project parameter in request context
15+
RPProjectContextKey contextKey = "rp_project" //nolint:gosec // This is a context key, not a credential
1416
)
1517

16-
// HTTPTokenMiddleware returns an HTTP middleware function that extracts RP API tokens
18+
// HTTPTokenMiddleware returns an HTTP middleware function that extracts RP API tokens and project parameters
1719
func HTTPTokenMiddleware(next http.Handler) http.Handler {
1820
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1921
// Extract RP API token from request headers
@@ -34,6 +36,25 @@ func HTTPTokenMiddleware(next http.Handler) http.Handler {
3436
"checked_headers", []string{"Authorization"})
3537
}
3638

39+
// Extract project parameter from request headers
40+
rpProject := extractRPProjectFromRequest(r)
41+
42+
if rpProject != "" {
43+
// Add project to request context for use by MCP handlers
44+
r = r.WithContext(WithProjectInContext(r.Context(), rpProject))
45+
46+
slog.Debug("Extracted RP project parameter from HTTP request",
47+
"source", "http_header",
48+
"method", r.Method,
49+
"path", r.URL.Path,
50+
"project", rpProject)
51+
} else {
52+
slog.Debug("No RP project parameter found in HTTP request headers",
53+
"method", r.Method,
54+
"path", r.URL.Path,
55+
"checked_headers", []string{"X-Project"})
56+
}
57+
3758
// Continue to next handler
3859
next.ServeHTTP(w, r)
3960
})
@@ -74,3 +95,30 @@ func GetTokenFromContext(ctx context.Context) (string, bool) {
7495
token, ok := ctx.Value(RPTokenContextKey).(string)
7596
return token, ok && token != ""
7697
}
98+
99+
// extractRPProjectFromRequest extracts RP project parameter from HTTP request headers
100+
// Supports X-Project header
101+
func extractRPProjectFromRequest(r *http.Request) string {
102+
project := strings.TrimSpace(r.Header.Get("X-Project"))
103+
if project != "" {
104+
slog.Debug("Valid RP project parameter extracted from request header",
105+
"source", "X-Project",
106+
"project", project)
107+
return project
108+
}
109+
return ""
110+
}
111+
112+
// WithProjectInContext adds RP project parameter to request context
113+
func WithProjectInContext(ctx context.Context, project string) context.Context {
114+
// Trim whitespace from project parameter
115+
project = strings.TrimSpace(project)
116+
return context.WithValue(ctx, RPProjectContextKey, project)
117+
}
118+
119+
// GetProjectFromContext extracts RP project parameter from request context
120+
func GetProjectFromContext(ctx context.Context) (string, bool) {
121+
project, ok := ctx.Value(RPProjectContextKey).(string)
122+
res := strings.TrimSpace(project)
123+
return res, ok && res != ""
124+
}
Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
package mcpreportportal
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestExtractRPProjectFromRequest(t *testing.T) {
13+
tests := []struct {
14+
name string
15+
headers map[string]string
16+
expectedResult string
17+
}{
18+
{
19+
name: "valid X-Project header",
20+
headers: map[string]string{"X-Project": "test-project"},
21+
expectedResult: "test-project",
22+
},
23+
{
24+
name: "X-Project header with whitespace",
25+
headers: map[string]string{"X-Project": " test-project "},
26+
expectedResult: "test-project",
27+
},
28+
{
29+
name: "empty X-Project header",
30+
headers: map[string]string{"X-Project": ""},
31+
expectedResult: "",
32+
},
33+
{
34+
name: "missing X-Project header",
35+
headers: map[string]string{},
36+
expectedResult: "",
37+
},
38+
{
39+
name: "X-Project header with only whitespace",
40+
headers: map[string]string{"X-Project": " "},
41+
expectedResult: "",
42+
},
43+
{
44+
name: "case insensitive header name",
45+
headers: map[string]string{"x-project": "test-project"},
46+
expectedResult: "test-project",
47+
},
48+
{
49+
name: "other headers present but no X-Project",
50+
headers: map[string]string{
51+
"Authorization": "Bearer token",
52+
"Content-Type": "application/json",
53+
},
54+
expectedResult: "",
55+
},
56+
}
57+
58+
for _, tt := range tests {
59+
t.Run(tt.name, func(t *testing.T) {
60+
req := httptest.NewRequest("GET", "/test", nil)
61+
62+
// Set headers
63+
for key, value := range tt.headers {
64+
req.Header.Set(key, value)
65+
}
66+
67+
result := extractRPProjectFromRequest(req)
68+
assert.Equal(t, tt.expectedResult, result)
69+
})
70+
}
71+
}
72+
73+
func TestWithProjectInContext(t *testing.T) {
74+
ctx := context.Background()
75+
project := "test-project"
76+
77+
// Test adding project to context
78+
ctxWithProject := WithProjectInContext(ctx, project)
79+
80+
// Test retrieving project from context
81+
retrievedProject, ok := GetProjectFromContext(ctxWithProject)
82+
assert.True(t, ok)
83+
assert.Equal(t, project, retrievedProject)
84+
85+
// Test that original context doesn't have project
86+
_, ok = GetProjectFromContext(ctx)
87+
assert.False(t, ok)
88+
}
89+
90+
func TestGetProjectFromContext(t *testing.T) {
91+
tests := []struct {
92+
name string
93+
contextValue interface{}
94+
expectedProject string
95+
expectedOk bool
96+
}{
97+
{
98+
name: "valid project string",
99+
contextValue: "test-project",
100+
expectedProject: "test-project",
101+
expectedOk: true,
102+
},
103+
{
104+
name: "empty project string",
105+
contextValue: "",
106+
expectedProject: "",
107+
expectedOk: false,
108+
},
109+
{
110+
name: "nil context value",
111+
contextValue: nil,
112+
expectedProject: "",
113+
expectedOk: false,
114+
},
115+
{
116+
name: "non-string context value",
117+
contextValue: 123,
118+
expectedProject: "",
119+
expectedOk: false,
120+
},
121+
{
122+
name: "context without project key",
123+
contextValue: nil, // This will be handled by the context not having the key
124+
expectedProject: "",
125+
expectedOk: false,
126+
},
127+
}
128+
129+
for _, tt := range tests {
130+
t.Run(tt.name, func(t *testing.T) {
131+
ctx := context.Background()
132+
133+
if tt.contextValue != nil {
134+
ctx = context.WithValue(ctx, RPProjectContextKey, tt.contextValue)
135+
}
136+
137+
project, ok := GetProjectFromContext(ctx)
138+
assert.Equal(t, tt.expectedOk, ok)
139+
assert.Equal(t, tt.expectedProject, project)
140+
})
141+
}
142+
}
143+
144+
func TestHTTPTokenMiddleware_ProjectExtraction(t *testing.T) {
145+
tests := []struct {
146+
name string
147+
headers map[string]string
148+
expectProject bool
149+
expectedProject string
150+
}{
151+
{
152+
name: "X-Project header present",
153+
headers: map[string]string{"X-Project": "test-project"},
154+
expectProject: true,
155+
expectedProject: "test-project",
156+
},
157+
{
158+
name: "X-Project header missing",
159+
headers: map[string]string{},
160+
expectProject: false,
161+
expectedProject: "",
162+
},
163+
{
164+
name: "X-Project header empty",
165+
headers: map[string]string{"X-Project": ""},
166+
expectProject: false,
167+
expectedProject: "",
168+
},
169+
{
170+
name: "X-Project header with whitespace",
171+
headers: map[string]string{"X-Project": " test-project "},
172+
expectProject: true,
173+
expectedProject: "test-project",
174+
},
175+
}
176+
177+
for _, tt := range tests {
178+
t.Run(tt.name, func(t *testing.T) {
179+
// Create a test handler that checks the context
180+
var capturedProject string
181+
var projectFound bool
182+
183+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
184+
project, ok := GetProjectFromContext(r.Context())
185+
capturedProject = project
186+
projectFound = ok
187+
w.WriteHeader(http.StatusOK)
188+
})
189+
190+
// Create middleware
191+
middleware := HTTPTokenMiddleware(testHandler)
192+
193+
// Create request with headers
194+
req := httptest.NewRequest("GET", "/test", nil)
195+
for key, value := range tt.headers {
196+
req.Header.Set(key, value)
197+
}
198+
199+
// Create response recorder
200+
rr := httptest.NewRecorder()
201+
202+
// Execute middleware
203+
middleware.ServeHTTP(rr, req)
204+
205+
// Verify response
206+
assert.Equal(t, http.StatusOK, rr.Code)
207+
208+
// Verify project extraction
209+
assert.Equal(t, tt.expectProject, projectFound)
210+
if tt.expectProject {
211+
assert.Equal(t, tt.expectedProject, capturedProject)
212+
}
213+
})
214+
}
215+
}
216+
217+
func TestHTTPTokenMiddleware_CombinedTokenAndProject(t *testing.T) {
218+
// Test that both token and project can be extracted simultaneously
219+
req := httptest.NewRequest("GET", "/test", nil)
220+
req.Header.Set("Authorization", "Bearer 1234567890123456")
221+
req.Header.Set("X-Project", "test-project")
222+
223+
var capturedToken, capturedProject string
224+
var tokenFound, projectFound bool
225+
226+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
227+
token, ok := GetTokenFromContext(r.Context())
228+
capturedToken = token
229+
tokenFound = ok
230+
231+
project, ok := GetProjectFromContext(r.Context())
232+
capturedProject = project
233+
projectFound = ok
234+
235+
w.WriteHeader(http.StatusOK)
236+
})
237+
238+
middleware := HTTPTokenMiddleware(testHandler)
239+
rr := httptest.NewRecorder()
240+
241+
middleware.ServeHTTP(rr, req)
242+
243+
// Verify both token and project were extracted
244+
assert.Equal(t, http.StatusOK, rr.Code)
245+
assert.True(t, tokenFound)
246+
assert.Equal(t, "1234567890123456", capturedToken)
247+
assert.True(t, projectFound)
248+
assert.Equal(t, "test-project", capturedProject)
249+
}
250+
251+
func TestHTTPTokenMiddleware_NoHeaders(t *testing.T) {
252+
// Test middleware with no headers
253+
req := httptest.NewRequest("GET", "/test", nil)
254+
255+
var tokenFound, projectFound bool
256+
257+
testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
258+
_, tokenFound = GetTokenFromContext(r.Context())
259+
_, projectFound = GetProjectFromContext(r.Context())
260+
w.WriteHeader(http.StatusOK)
261+
})
262+
263+
middleware := HTTPTokenMiddleware(testHandler)
264+
rr := httptest.NewRecorder()
265+
266+
middleware.ServeHTTP(rr, req)
267+
268+
// Verify no token or project were found
269+
assert.Equal(t, http.StatusOK, rr.Code)
270+
assert.False(t, tokenFound)
271+
assert.False(t, projectFound)
272+
}

0 commit comments

Comments
 (0)