mirror of
				https://github.com/tailscale/tailscale.git
				synced 2025-11-04 02:01:14 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			549 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			549 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright (c) Tailscale Inc & AUTHORS
 | 
						|
// SPDX-License-Identifier: BSD-3-Clause
 | 
						|
 | 
						|
//go:build !plan9
 | 
						|
 | 
						|
package apiproxy
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"net/http/httptest"
 | 
						|
	"net/netip"
 | 
						|
	"net/url"
 | 
						|
	"reflect"
 | 
						|
	"testing"
 | 
						|
 | 
						|
	"go.uber.org/zap"
 | 
						|
	"tailscale.com/client/tailscale/apitype"
 | 
						|
	"tailscale.com/net/netx"
 | 
						|
	"tailscale.com/sessionrecording"
 | 
						|
	"tailscale.com/tailcfg"
 | 
						|
	"tailscale.com/tsnet"
 | 
						|
)
 | 
						|
 | 
						|
type fakeSender struct {
 | 
						|
	sent  map[netip.AddrPort][]byte
 | 
						|
	err   error
 | 
						|
	calls int
 | 
						|
}
 | 
						|
 | 
						|
func (s *fakeSender) Send(ap netip.AddrPort, event io.Reader, dial netx.DialFunc) error {
 | 
						|
	s.calls++
 | 
						|
	if s.err != nil {
 | 
						|
		return s.err
 | 
						|
	}
 | 
						|
	if s.sent == nil {
 | 
						|
		s.sent = make(map[netip.AddrPort][]byte)
 | 
						|
	}
 | 
						|
	data, _ := io.ReadAll(event)
 | 
						|
	s.sent[ap] = data
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (s *fakeSender) Reset() {
 | 
						|
	s.sent = nil
 | 
						|
	s.err = nil
 | 
						|
	s.calls = 0
 | 
						|
}
 | 
						|
 | 
						|
func TestRecordRequestAsEvent(t *testing.T) {
 | 
						|
	zl, err := zap.NewDevelopment()
 | 
						|
	if err != nil {
 | 
						|
		t.Fatal(err)
 | 
						|
	}
 | 
						|
 | 
						|
	sender := &fakeSender{}
 | 
						|
	ap := &APIServerProxy{
 | 
						|
		log:           zl.Sugar(),
 | 
						|
		ts:            &tsnet.Server{},
 | 
						|
		sendEventFunc: sender.Send,
 | 
						|
	}
 | 
						|
 | 
						|
	defaultWho := &apitype.WhoIsResponse{
 | 
						|
		Node: &tailcfg.Node{
 | 
						|
			StableID: "stable-id",
 | 
						|
			Name:     "node.ts.net.",
 | 
						|
		},
 | 
						|
		UserProfile: &tailcfg.UserProfile{
 | 
						|
			ID:        1,
 | 
						|
			LoginName: "user@example.com",
 | 
						|
		},
 | 
						|
		CapMap: tailcfg.PeerCapMap{
 | 
						|
			tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{
 | 
						|
				tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234"]}`),
 | 
						|
				tailcfg.RawMessage(`{"enforceRecorder": true}`),
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	defaultSource := sessionrecording.Source{
 | 
						|
		Node:       "node.ts.net",
 | 
						|
		NodeID:     "stable-id",
 | 
						|
		NodeUser:   "user@example.com",
 | 
						|
		NodeUserID: 1,
 | 
						|
	}
 | 
						|
 | 
						|
	tests := []struct {
 | 
						|
		name         string
 | 
						|
		req          func() *http.Request
 | 
						|
		who          *apitype.WhoIsResponse
 | 
						|
		setupSender  func()
 | 
						|
		wantErr      bool
 | 
						|
		wantEvent    *sessionrecording.Event
 | 
						|
		wantNumCalls int
 | 
						|
	}{
 | 
						|
		{
 | 
						|
			name: "request-with-dot-in-name",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/namespaces/default/pods/foo.bar", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/namespaces/default/pods/foo.bar",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/namespaces/default/pods/foo.bar",
 | 
						|
					Verb:              "get",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Namespace:         "default",
 | 
						|
					Resource:          "pods",
 | 
						|
					Name:              "foo.bar",
 | 
						|
					Parts:             []string{"pods", "foo.bar"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-with-dash-in-name",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/namespaces/default/pods/foo-bar", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/namespaces/default/pods/foo-bar",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/namespaces/default/pods/foo-bar",
 | 
						|
					Verb:              "get",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Namespace:         "default",
 | 
						|
					Resource:          "pods",
 | 
						|
					Name:              "foo-bar",
 | 
						|
					Parts:             []string{"pods", "foo-bar"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-with-query-parameter",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods?watch=true", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/pods?watch=true",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{"watch": []string{"true"}},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/pods",
 | 
						|
					Verb:              "watch",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "pods",
 | 
						|
					Parts:             []string{"pods"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-with-label-selector",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods?labelSelector=app%3Dfoo", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/pods?labelSelector=app%3Dfoo",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{"labelSelector": []string{"app=foo"}},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/pods",
 | 
						|
					Verb:              "list",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "pods",
 | 
						|
					Parts:             []string{"pods"},
 | 
						|
					LabelSelector:     "app=foo",
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-with-field-selector",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods?fieldSelector=status.phase%3DRunning", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/pods?fieldSelector=status.phase%3DRunning",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{"fieldSelector": []string{"status.phase=Running"}},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/pods",
 | 
						|
					Verb:              "list",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "pods",
 | 
						|
					Parts:             []string{"pods"},
 | 
						|
					FieldSelector:     "status.phase=Running",
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-for-non-existent-resource",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/foo", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/foo",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/foo",
 | 
						|
					Verb:              "list",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "foo",
 | 
						|
					Parts:             []string{"foo"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "basic-request",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/pods",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/pods",
 | 
						|
					Verb:              "list",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "pods",
 | 
						|
					Parts:             []string{"pods"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "multiple-recorders",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods", nil)
 | 
						|
			},
 | 
						|
			who: &apitype.WhoIsResponse{
 | 
						|
				Node:        defaultWho.Node,
 | 
						|
				UserProfile: defaultWho.UserProfile,
 | 
						|
				CapMap: tailcfg.PeerCapMap{
 | 
						|
					tailcfg.PeerCapabilityKubernetes: []tailcfg.RawMessage{
 | 
						|
						tailcfg.RawMessage(`{"recorderAddrs":["127.0.0.1:1234", "127.0.0.1:5678"]}`),
 | 
						|
					},
 | 
						|
				},
 | 
						|
			},
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-with-body",
 | 
						|
			req: func() *http.Request {
 | 
						|
				req := httptest.NewRequest("POST", "/api/v1/pods", bytes.NewBufferString(`{"foo":"bar"}`))
 | 
						|
				req.Header.Set("Content-Type", "application/json")
 | 
						|
				return req
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "POST",
 | 
						|
					Path:            "/api/v1/pods",
 | 
						|
					Body:            json.RawMessage(`{"foo":"bar"}`),
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/pods",
 | 
						|
					Verb:              "create",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "pods",
 | 
						|
					Parts:             []string{"pods"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "tagged-node",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods", nil)
 | 
						|
			},
 | 
						|
			who: &apitype.WhoIsResponse{
 | 
						|
				Node: &tailcfg.Node{
 | 
						|
					StableID: "stable-id",
 | 
						|
					Name:     "node.ts.net.",
 | 
						|
					Tags:     []string{"tag:foo"},
 | 
						|
				},
 | 
						|
				UserProfile: &tailcfg.UserProfile{},
 | 
						|
				CapMap:      defaultWho.CapMap,
 | 
						|
			},
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/pods",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/pods",
 | 
						|
					Verb:              "list",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "pods",
 | 
						|
					Parts:             []string{"pods"},
 | 
						|
				},
 | 
						|
				Source: sessionrecording.Source{
 | 
						|
					Node:     "node.ts.net",
 | 
						|
					NodeID:   "stable-id",
 | 
						|
					NodeTags: []string{"tag:foo"},
 | 
						|
				},
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "no-recorders",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods", nil)
 | 
						|
			},
 | 
						|
			who: &apitype.WhoIsResponse{
 | 
						|
				Node:        defaultWho.Node,
 | 
						|
				UserProfile: defaultWho.UserProfile,
 | 
						|
				CapMap:      tailcfg.PeerCapMap{},
 | 
						|
			},
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 0,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "error-sending",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/pods", nil)
 | 
						|
			},
 | 
						|
			who: defaultWho,
 | 
						|
			setupSender: func() {
 | 
						|
				sender.Reset()
 | 
						|
				sender.err = errors.New("send error")
 | 
						|
			},
 | 
						|
			wantErr:      true,
 | 
						|
			wantNumCalls: 1,
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-for-crd",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/apis/custom.example.com/v1/myresources", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/apis/custom.example.com/v1/myresources",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/apis/custom.example.com/v1/myresources",
 | 
						|
					Verb:              "list",
 | 
						|
					APIPrefix:         "apis",
 | 
						|
					APIGroup:          "custom.example.com",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Resource:          "myresources",
 | 
						|
					Parts:             []string{"myresources"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-with-proxy-verb",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/namespaces/default/pods/foo/proxy", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/namespaces/default/pods/foo/proxy",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/namespaces/default/pods/foo/proxy",
 | 
						|
					Verb:              "get",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Namespace:         "default",
 | 
						|
					Resource:          "pods",
 | 
						|
					Subresource:       "proxy",
 | 
						|
					Name:              "foo",
 | 
						|
					Parts:             []string{"pods", "foo", "proxy"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
		{
 | 
						|
			name: "request-with-complex-path",
 | 
						|
			req: func() *http.Request {
 | 
						|
				return httptest.NewRequest("GET", "/api/v1/namespaces/default/services/foo:8080/proxy-subpath/more/segments", nil)
 | 
						|
			},
 | 
						|
			who:          defaultWho,
 | 
						|
			setupSender:  func() { sender.Reset() },
 | 
						|
			wantNumCalls: 1,
 | 
						|
			wantEvent: &sessionrecording.Event{
 | 
						|
				Type: sessionrecording.KubernetesAPIEventType,
 | 
						|
				Request: sessionrecording.Request{
 | 
						|
					Method:          "GET",
 | 
						|
					Path:            "/api/v1/namespaces/default/services/foo:8080/proxy-subpath/more/segments",
 | 
						|
					Body:            nil,
 | 
						|
					QueryParameters: url.Values{},
 | 
						|
				},
 | 
						|
				Kubernetes: sessionrecording.KubernetesRequestInfo{
 | 
						|
					IsResourceRequest: true,
 | 
						|
					Path:              "/api/v1/namespaces/default/services/foo:8080/proxy-subpath/more/segments",
 | 
						|
					Verb:              "get",
 | 
						|
					APIPrefix:         "api",
 | 
						|
					APIVersion:        "v1",
 | 
						|
					Namespace:         "default",
 | 
						|
					Resource:          "services",
 | 
						|
					Subresource:       "proxy-subpath",
 | 
						|
					Name:              "foo:8080",
 | 
						|
					Parts:             []string{"services", "foo:8080", "proxy-subpath", "more", "segments"},
 | 
						|
				},
 | 
						|
				Source: defaultSource,
 | 
						|
			},
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	for _, tt := range tests {
 | 
						|
		t.Run(tt.name, func(t *testing.T) {
 | 
						|
			tt.setupSender()
 | 
						|
 | 
						|
			req := tt.req()
 | 
						|
			err := ap.recordRequestAsEvent(req, tt.who)
 | 
						|
 | 
						|
			if (err != nil) != tt.wantErr {
 | 
						|
				t.Fatalf("recordRequestAsEvent() error = %v, wantErr %v", err, tt.wantErr)
 | 
						|
			}
 | 
						|
 | 
						|
			if sender.calls != tt.wantNumCalls {
 | 
						|
				t.Fatalf("expected %d calls to sender, got %d", tt.wantNumCalls, sender.calls)
 | 
						|
			}
 | 
						|
 | 
						|
			if tt.wantEvent != nil {
 | 
						|
				for _, sentData := range sender.sent {
 | 
						|
					var got sessionrecording.Event
 | 
						|
					if err := json.Unmarshal(sentData, &got); err != nil {
 | 
						|
						t.Fatalf("failed to unmarshal sent event: %v", err)
 | 
						|
					}
 | 
						|
 | 
						|
					got.Timestamp = 0
 | 
						|
					tt.wantEvent.Timestamp = got.Timestamp
 | 
						|
 | 
						|
					got.UserAgent = ""
 | 
						|
					tt.wantEvent.UserAgent = ""
 | 
						|
 | 
						|
					if !bytes.Equal(got.Request.Body, tt.wantEvent.Request.Body) {
 | 
						|
						t.Errorf("sent event body does not match wanted event body.\nGot:  %s\nWant: %s", string(got.Request.Body), string(tt.wantEvent.Request.Body))
 | 
						|
					}
 | 
						|
					got.Request.Body = nil
 | 
						|
					tt.wantEvent.Request.Body = nil
 | 
						|
 | 
						|
					if !reflect.DeepEqual(&got, tt.wantEvent) {
 | 
						|
						t.Errorf("sent event does not match wanted event.\nGot:  %#v\nWant: %#v", &got, tt.wantEvent)
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		})
 | 
						|
	}
 | 
						|
}
 |